Skip to main content

bisket_vr_debug/
bisket-vr-debug.rs

1//! bisket-vr-debug — headless verification harness for head-driven AVC.
2//!
3//! See `examples/bisket-vr-debug.mms` for scene setup.
4//!
5//! What this binary does:
6//!   1. Loads the scene (two bisket avatars: one bare-FK reference, one
7//!      AVC-driven).
8//!   2. Ticks the engine enough to spawn armatures and let AVC wire its
9//!      splices + IK chains.
10//!   3. Runs the original spine probes against scripted head poses.
11//!   4. Resolves the live arm IK chain(s), then scripts wrist/pronation
12//!      probes by mutating the actual runtime hand targets directly.
13//!   5. Optionally compares authored hand target offsets and
14//!      `copy_end_rotation` modes without changing engine behavior outside
15//!      this harness process.
16//!
17//! Run with `cargo run --release --example bisket-vr-debug`.
18//! Useful flags:
19//!   --compare-copy-end-rotation
20//!   --neutralize-hand-offsets
21//!   --compare-hand-offsets
22
23use std::env;
24
25use mittens_engine::engine::ecs::component::{
26    AvatarControlComponent, IKChainComponent, IKSolver, TransformComponent,
27};
28use mittens_engine::engine::ecs::{ComponentId, IntentValue, SignalEmitter, World};
29use mittens_engine::engine::user_input::InputState;
30use mittens_engine::utils::math::{
31    mat_to_quat, mat4_identity, mat4_inverse, mat4_mul_vec4, quat_conjugate, quat_from_axis_angle,
32    quat_mul, quat_rotate_vec3, quat_rotation_y, quat_to_axis_angle, vec3_len, vec3_normalize,
33    vec3_sub,
34};
35use mittens_engine::{engine, scripting, utils};
36
37const SPINE_BONES: &[&str] = &[
38    "J_Bip_C_Hips",
39    "J_Bip_C_Spine",
40    "J_Bip_C_Chest",
41    "J_Bip_C_UpperChest",
42    "J_Bip_C_Neck",
43    "J_Bip_C_Head",
44];
45
46const BONE_LENGTH_TOL: f32 = 0.005;
47const SPLICE_TARGET_TOL: f32 = 0.010;
48const MONOTONIC_Y_TOL: f32 = 0.005;
49const SETTLE_TICKS_PER_POSE: usize = 8;
50const WRIST_TWIST_RAD: f32 = 1.35;
51
52#[derive(Clone, Copy)]
53struct Pose {
54    name: &'static str,
55    description: &'static str,
56    t: [f32; 3],
57    rot: [f32; 4],
58}
59
60#[derive(Clone, Copy, PartialEq, Eq)]
61enum ArmSide {
62    Left,
63    Right,
64}
65
66impl ArmSide {
67    fn as_str(self) -> &'static str {
68        match self {
69            Self::Left => "left",
70            Self::Right => "right",
71        }
72    }
73}
74
75#[derive(Clone, Copy)]
76enum HandOffsetMode {
77    Authored,
78    Neutralized,
79}
80
81impl HandOffsetMode {
82    fn as_str(self) -> &'static str {
83        match self {
84            Self::Authored => "authored_offsets",
85            Self::Neutralized => "neutralized_offsets",
86        }
87    }
88}
89
90#[derive(Clone, Copy)]
91enum CopyEndRotationMode {
92    SolverDefault,
93    ForcedOff,
94}
95
96impl CopyEndRotationMode {
97    fn as_str(self) -> &'static str {
98        match self {
99            Self::SolverDefault => "copy_end_rotation=solver_default",
100            Self::ForcedOff => "copy_end_rotation=forced_off",
101        }
102    }
103}
104
105#[derive(Clone, Copy)]
106struct ArmProbePose {
107    name: &'static str,
108    description: &'static str,
109    left_twist_rad: f32,
110    right_twist_rad: f32,
111}
112
113#[derive(Clone, Copy)]
114struct HarnessOptions {
115    compare_copy_end_rotation: bool,
116    compare_hand_offsets: bool,
117    neutralize_hand_offsets: bool,
118}
119
120#[derive(Clone, Copy)]
121struct ArmChainRuntime {
122    side: ArmSide,
123    ik_chain_id: ComponentId,
124    upper_arm: ComponentId,
125    lower_arm: ComponentId,
126    hand: ComponentId,
127    target: ComponentId,
128    authored_target_local_rotation: [f32; 4],
129    rest_hand_world_pos: [f32; 3],
130    original_copy_end_rotation: bool,
131}
132
133#[derive(Clone, Copy)]
134struct ArmPassBaseline {
135    hand_world_pos: [f32; 3],
136    target_world_rot: [f32; 4],
137    forearm_axis_world: [f32; 3],
138}
139
140#[derive(Clone, Copy)]
141struct NodeRotationSample {
142    world: [f32; 4],
143    local: [f32; 4],
144}
145
146#[derive(Clone, Copy)]
147struct ArmPoseSample {
148    upper_arm: NodeRotationSample,
149    lower_arm: NodeRotationSample,
150    hand: NodeRotationSample,
151    target: NodeRotationSample,
152    lower_to_hand_deg: f32,
153    lower_to_target_deg: f32,
154    hand_to_target_deg: f32,
155}
156
157#[derive(Clone, Copy)]
158struct MirrorSignature {
159    lower_to_hand_deg: f32,
160    lower_to_target_deg: f32,
161    hand_to_target_deg: f32,
162}
163
164fn main() {
165    mittens_engine::example_support::ensure_model_assets();
166    utils::logger::init();
167    let opts = parse_args();
168
169    let world = engine::ecs::World::default();
170    let mut universe = engine::Universe::new(world);
171
172    let source = include_str!("bisket-vr-debug.mms");
173    let output = scripting::MeowMeowRunner::eval_with_world_at_path(
174        source,
175        Some("examples/bisket-vr-debug.mms"),
176        &mut universe.world,
177        &mut universe.systems.rx,
178        &mut universe.command_queue,
179    );
180    for e in &output.errors {
181        eprintln!("[mms] {e}");
182    }
183    println!("[mms] {} intent(s)", output.intents.len());
184    for intent in output.intents {
185        universe
186            .command_queue
187            .push_intent_now(ComponentId::default(), intent);
188    }
189    drain(&mut universe);
190    tick_gltf(&mut universe);
191    drain(&mut universe);
192
193    let avc_id = universe
194        .world
195        .all_components()
196        .find(|&id| {
197            universe
198                .world
199                .get_component_by_id_as::<AvatarControlComponent>(id)
200                .is_some()
201        })
202        .expect("AvatarControlComponent not found — scene didn't load?");
203    let driven_t = universe
204        .world
205        .parent_of(avc_id)
206        .expect("AVC has no parent (driven_t)");
207
208    println!("[debug] avc_id={:?} driven_t={:?}", avc_id, driven_t);
209    println!(
210        "[debug] options: compare_copy_end_rotation={} compare_hand_offsets={} neutralize_hand_offsets={}",
211        opts.compare_copy_end_rotation, opts.compare_hand_offsets, opts.neutralize_hand_offsets
212    );
213
214    let head_target_offset_world = head_target_offset_in_target_local(&universe.world, avc_id);
215    println!(
216        "[debug] head_target_offset (target-local) = {:?}",
217        head_target_offset_world
218    );
219
220    settle_world(&mut universe, 10);
221
222    let (ref_bones, avc_bones) = resolve_spine_bones(&universe.world, avc_id);
223    println!(
224        "[debug] resolved {} spine bones in both subtrees",
225        SPINE_BONES.len()
226    );
227
228    let avc_head_bone = avc_bones[SPINE_BONES
229        .iter()
230        .position(|n| *n == "J_Bip_C_Head")
231        .unwrap()];
232    let splice_head = universe
233        .world
234        .parent_of(avc_head_bone)
235        .expect("head_bone has no parent — splice didn't run?");
236
237    run_spine_probes(
238        &mut universe,
239        driven_t,
240        splice_head,
241        &ref_bones,
242        &avc_bones,
243        head_target_offset_world,
244    );
245
246    let arms = resolve_arm_chains(&universe.world, avc_id);
247    print_arm_startup_report(&universe.world, &arms);
248    run_arm_probes(&mut universe, driven_t, &arms, opts);
249
250    println!("\n[debug] done. Exiting (no window loop).");
251}
252
253fn parse_args() -> HarnessOptions {
254    let mut opts = HarnessOptions {
255        compare_copy_end_rotation: false,
256        compare_hand_offsets: false,
257        neutralize_hand_offsets: false,
258    };
259    for arg in env::args().skip(1) {
260        match arg.as_str() {
261            "--compare-copy-end-rotation" => opts.compare_copy_end_rotation = true,
262            "--compare-hand-offsets" => opts.compare_hand_offsets = true,
263            "--neutralize-hand-offsets" => opts.neutralize_hand_offsets = true,
264            other => {
265                eprintln!("[debug] ignoring unknown arg: {other}");
266            }
267        }
268    }
269    opts
270}
271
272fn run_spine_probes(
273    universe: &mut engine::Universe,
274    driven_t: ComponentId,
275    splice_head: ComponentId,
276    ref_bones: &[ComponentId],
277    avc_bones: &[ComponentId],
278    head_target_offset_world: [f32; 3],
279) {
280    let poses = [
281        Pose {
282            name: "rest",
283            description: "driven_t at HMD height (1.55m), identity rotation",
284            t: [0.0, 1.55, 0.0],
285            rot: ident_quat(),
286        },
287        Pose {
288            name: "pitch_up_30",
289            description: "head pitched up 30° around X",
290            t: [0.0, 1.55, 0.0],
291            rot: quat_from_axis_angle([1.0, 0.0, 0.0], 0.5236),
292        },
293        Pose {
294            name: "pitch_dn_30",
295            description: "head pitched down 30° around X",
296            t: [0.0, 1.55, 0.0],
297            rot: quat_from_axis_angle([1.0, 0.0, 0.0], -0.5236),
298        },
299        Pose {
300            name: "yaw_right_45",
301            description: "head yawed right 45° around Y",
302            t: [0.0, 1.55, 0.0],
303            rot: quat_from_axis_angle([0.0, 1.0, 0.0], 0.7854),
304        },
305        Pose {
306            name: "lean_forward",
307            description: "driven_t translated +0.2m on Z (head leans forward over toes)",
308            t: [0.0, 1.45, 0.20],
309            rot: ident_quat(),
310        },
311        Pose {
312            name: "crouch",
313            description: "driven_t lowered to 1.10m (player crouches)",
314            t: [0.0, 1.10, 0.0],
315            rot: ident_quat(),
316        },
317        Pose {
318            name: "walk_forward_0.5m",
319            description: "driven_t walks +0.5m on -Z (OpenXR forward = -Z)",
320            t: [0.0, 1.55, -0.5],
321            rot: ident_quat(),
322        },
323        Pose {
324            name: "walk_strafe_right_0.3m",
325            description: "driven_t side-step +0.3m on X",
326            t: [0.3, 1.55, 0.0],
327            rot: ident_quat(),
328        },
329    ];
330
331    let ref_y_z: Vec<[f32; 2]> = ref_bones
332        .iter()
333        .map(|&id| world_yz(&universe.world, id))
334        .collect();
335
336    for pose in &poses {
337        set_local_transform(
338            &mut universe.command_queue,
339            driven_t,
340            pose.t,
341            pose.rot,
342            [1.0, 1.0, 1.0],
343        );
344        settle_world(universe, SETTLE_TICKS_PER_POSE);
345
346        let avc_y_z: Vec<[f32; 2]> = avc_bones
347            .iter()
348            .map(|&id| world_yz(&universe.world, id))
349            .collect();
350        let avc_pos: Vec<[f32; 3]> = avc_bones
351            .iter()
352            .map(|&id| world_pos(&universe.world, id))
353            .collect();
354        let splice_pos = world_pos(&universe.world, splice_head);
355        let driven_pos = world_pos(&universe.world, driven_t);
356        let driven_rot = world_rot(&universe.world, driven_t);
357
358        println!("\n================================================================");
359        println!("spine pose: {}", pose.name);
360        println!("  {}", pose.description);
361        println!(
362            "  driven_t world pos: [{:+.3}, {:+.3}, {:+.3}] (model offset +1.2 on X)",
363            driven_pos[0], driven_pos[1], driven_pos[2]
364        );
365
366        println!("\nspine bones — model-local Y/Z (REF vs AVC)");
367        println!(
368            "  {:<22}  {:>7}  {:>7}    {:>7}  {:>7}     {:>+7}  {:>+7}",
369            "bone", "ref_y", "ref_z", "avc_y", "avc_z", "Δy", "Δz",
370        );
371        for (i, name) in SPINE_BONES.iter().enumerate() {
372            let r = ref_y_z[i];
373            let a = avc_y_z[i];
374            println!(
375                "  {:<22}  {:>+7.3}  {:>+7.3}    {:>+7.3}  {:>+7.3}     {:>+7.3}  {:>+7.3}",
376                name,
377                r[0],
378                r[1],
379                a[0],
380                a[1],
381                a[0] - r[0],
382                a[1] - r[1],
383            );
384        }
385
386        println!("\ninvariants");
387
388        let ref_seg_lens = segment_lengths(ref_bones, &universe.world);
389        let avc_seg_lens = segment_lengths(avc_bones, &universe.world);
390        let mut bone_length_ok = true;
391        for i in 0..ref_seg_lens.len() {
392            let drift = (avc_seg_lens[i] - ref_seg_lens[i]).abs();
393            let tag = if drift <= BONE_LENGTH_TOL {
394                "ok"
395            } else {
396                "FAIL"
397            };
398            if drift > BONE_LENGTH_TOL {
399                bone_length_ok = false;
400            }
401            println!(
402                "  bone_length  {:<14}→{:<14}  ref={:.4}  avc={:.4}  drift={:+.4}  {}",
403                SPINE_BONES[i],
404                SPINE_BONES[i + 1],
405                ref_seg_lens[i],
406                avc_seg_lens[i],
407                avc_seg_lens[i] - ref_seg_lens[i],
408                tag,
409            );
410        }
411        if bone_length_ok {
412            println!(
413                "  → all spine bone-lengths preserved within {:.0}mm",
414                BONE_LENGTH_TOL * 1000.0
415            );
416        }
417
418        let mut mono_ok = true;
419        let mut prev_y = f32::NEG_INFINITY;
420        for (i, name) in SPINE_BONES.iter().enumerate() {
421            if *name == "J_Bip_C_Head" {
422                break;
423            }
424            let y = avc_pos[i][1];
425            if y + MONOTONIC_Y_TOL < prev_y {
426                println!(
427                    "  monotonic_y  FAIL at {}: y={:+.3} drops below prev {:+.3}",
428                    name, y, prev_y
429                );
430                mono_ok = false;
431            }
432            prev_y = y;
433        }
434        if mono_ok {
435            println!(
436                "  monotonic_y  ok (hips → neck all non-decreasing within {:.0}mm)",
437                MONOTONIC_Y_TOL * 1000.0
438            );
439        }
440
441        let hips_idx = SPINE_BONES
442            .iter()
443            .position(|n| *n == "J_Bip_C_Hips")
444            .unwrap();
445        let hips_xz = [avc_pos[hips_idx][0], avc_pos[hips_idx][2]];
446        let dx = hips_xz[0] - splice_pos[0];
447        let dz = hips_xz[1] - splice_pos[2];
448        let xz_drift = (dx * dx + dz * dz).sqrt();
449        const HIPS_UNDER_HEAD_TOL: f32 = 0.050;
450        println!(
451            "  hips_under_head  splice_xz=[{:+.3},{:+.3}]  hips_xz=[{:+.3},{:+.3}]  drift={:.4}m  {}",
452            splice_pos[0],
453            splice_pos[2],
454            hips_xz[0],
455            hips_xz[1],
456            xz_drift,
457            if xz_drift <= HIPS_UNDER_HEAD_TOL {
458                "ok"
459            } else {
460                "FAIL"
461            },
462        );
463
464        let predicted_splice_world = {
465            let off_world = quat_rotate_vec3(driven_rot, head_target_offset_world);
466            [
467                driven_pos[0] + off_world[0],
468                driven_pos[1] + off_world[1],
469                driven_pos[2] + off_world[2],
470            ]
471        };
472        let splice_drift = vec3_dist(splice_pos, predicted_splice_world);
473        println!(
474            "  splice_head  expected=[{:+.3},{:+.3},{:+.3}]  actual=[{:+.3},{:+.3},{:+.3}]  drift={:.4}m  {}",
475            predicted_splice_world[0],
476            predicted_splice_world[1],
477            predicted_splice_world[2],
478            splice_pos[0],
479            splice_pos[1],
480            splice_pos[2],
481            splice_drift,
482            if splice_drift <= SPLICE_TARGET_TOL {
483                "ok"
484            } else {
485                "FAIL"
486            },
487        );
488    }
489}
490
491fn resolve_spine_bones(
492    world: &World,
493    _avc_id: ComponentId,
494) -> (Vec<ComponentId>, Vec<ComponentId>) {
495    let mut ref_bones = Vec::with_capacity(SPINE_BONES.len());
496    let mut avc_bones = Vec::with_capacity(SPINE_BONES.len());
497    for name in SPINE_BONES {
498        let all = find_components_by_name(world, name);
499        let (avc_side, ref_side): (Vec<_>, Vec<_>) = all
500            .into_iter()
501            .partition(|&id| world_pos(world, id)[0] > 0.0);
502        let avc = avc_side
503            .first()
504            .copied()
505            .unwrap_or_else(|| panic!("bone {} not found on +X AVC side", name));
506        let reference = ref_side
507            .first()
508            .copied()
509            .unwrap_or_else(|| panic!("bone {} not found on -X reference side", name));
510        ref_bones.push(reference);
511        avc_bones.push(avc);
512    }
513    (ref_bones, avc_bones)
514}
515
516fn resolve_arm_chains(world: &World, avc_id: ComponentId) -> Vec<ArmChainRuntime> {
517    let avc = world
518        .get_component_by_id_as::<AvatarControlComponent>(avc_id)
519        .expect("avc missing");
520    let hand_map = [
521        (
522            ArmSide::Left,
523            avc.left_hand_bone
524                .as_deref()
525                .expect("left_hand_bone config missing"),
526        ),
527        (
528            ArmSide::Right,
529            avc.right_hand_bone
530                .as_deref()
531                .expect("right_hand_bone config missing"),
532        ),
533    ];
534
535    hand_map
536        .into_iter()
537        .map(|(side, hand_name)| {
538            let hand_id = find_components_by_name(world, hand_name)
539                .into_iter()
540                .find(|&id| is_descendant_of(world, id, avc_id))
541                .unwrap_or_else(|| panic!("{} bone not found under AVC subtree", hand_name));
542            let (ik_chain_id, solver, target_id) = world
543                .all_components()
544                .find_map(|id| {
545                    let ik = world.get_component_by_id_as::<IKChainComponent>(id)?;
546                    if !is_descendant_of(world, id, avc_id) {
547                        return None;
548                    }
549                    if ik.end_effector_id != hand_id {
550                        return None;
551                    }
552                    Some((id, ik.solver, ik.target_id))
553                })
554                .unwrap_or_else(|| panic!("{} arm IK chain not found", side.as_str()));
555            let (upper_arm, lower_arm, copy_end_rotation) = match solver {
556                IKSolver::TwoBoneIK {
557                    root_joint_id,
558                    mid_joint_id,
559                    copy_end_rotation,
560                    ..
561                } => (root_joint_id, mid_joint_id, copy_end_rotation),
562                _ => panic!("{} arm chain resolved to non-TwoBoneIK", side.as_str()),
563            };
564            let authored_target_local_rotation = local_rot(world, target_id);
565            let rest_hand_world_pos = world_pos(world, hand_id);
566
567            ArmChainRuntime {
568                side,
569                ik_chain_id,
570                upper_arm,
571                lower_arm,
572                hand: hand_id,
573                target: target_id,
574                authored_target_local_rotation,
575                rest_hand_world_pos,
576                original_copy_end_rotation: copy_end_rotation,
577            }
578        })
579        .collect()
580}
581
582fn print_arm_startup_report(world: &World, arms: &[ArmChainRuntime]) {
583    println!("\n================================================================");
584    println!("arm chain startup");
585    for arm in arms {
586        let upward = upward_chain_labels(world, arm.hand, Some(arm.upper_arm), 8);
587        let skipped = skipped_chain_labels(world, arm.hand, arm.lower_arm, arm.upper_arm);
588        println!(
589            "  [{}] ik_chain={} target={} upper={} lower={} hand={}",
590            arm.side.as_str(),
591            describe_component(world, arm.ik_chain_id),
592            describe_component(world, arm.target),
593            describe_component(world, arm.upper_arm),
594            describe_component(world, arm.lower_arm),
595            describe_component(world, arm.hand),
596        );
597        println!("    hand→upper chain: {}", upward.join(" <- "));
598        if skipped.is_empty() {
599            println!("    skipped between hand and upper: none (direct upper->lower->hand)");
600        } else {
601            println!(
602                "    skipped between hand and upper: {}",
603                skipped.join(" <- ")
604            );
605        }
606        println!(
607            "    target parent chain: {}",
608            upward_chain_labels(world, arm.target, None, 4).join(" <- ")
609        );
610    }
611}
612
613fn run_arm_probes(
614    universe: &mut engine::Universe,
615    driven_t: ComponentId,
616    arms: &[ArmChainRuntime],
617    opts: HarnessOptions,
618) {
619    let hand_offset_modes: Vec<HandOffsetMode> = if opts.compare_hand_offsets {
620        vec![HandOffsetMode::Authored, HandOffsetMode::Neutralized]
621    } else if opts.neutralize_hand_offsets {
622        vec![HandOffsetMode::Neutralized]
623    } else {
624        vec![HandOffsetMode::Authored]
625    };
626    let copy_modes: Vec<CopyEndRotationMode> = if opts.compare_copy_end_rotation {
627        vec![
628            CopyEndRotationMode::SolverDefault,
629            CopyEndRotationMode::ForcedOff,
630        ]
631    } else {
632        vec![CopyEndRotationMode::SolverDefault]
633    };
634
635    let poses = [
636        ArmProbePose {
637            name: "neutral_rest",
638            description: "both hand targets at rest hand position and pass baseline rotation",
639            left_twist_rad: 0.0,
640            right_twist_rad: 0.0,
641        },
642        ArmProbePose {
643            name: "left_palm_down",
644            description: "left hand target pronated around the current forearm axis",
645            left_twist_rad: -WRIST_TWIST_RAD,
646            right_twist_rad: 0.0,
647        },
648        ArmProbePose {
649            name: "left_palm_up",
650            description: "left hand target supinated around the current forearm axis",
651            left_twist_rad: WRIST_TWIST_RAD,
652            right_twist_rad: 0.0,
653        },
654        ArmProbePose {
655            name: "right_palm_down",
656            description: "right hand target pronated around the current forearm axis",
657            left_twist_rad: 0.0,
658            right_twist_rad: WRIST_TWIST_RAD,
659        },
660        ArmProbePose {
661            name: "right_palm_up",
662            description: "right hand target supinated around the current forearm axis",
663            left_twist_rad: 0.0,
664            right_twist_rad: -WRIST_TWIST_RAD,
665        },
666    ];
667
668    set_local_transform(
669        &mut universe.command_queue,
670        driven_t,
671        [0.0, 1.55, 0.0],
672        ident_quat(),
673        [1.0, 1.0, 1.0],
674    );
675    settle_world(universe, SETTLE_TICKS_PER_POSE);
676
677    for hand_mode in hand_offset_modes {
678        for copy_mode in &copy_modes {
679            println!("\n================================================================");
680            println!("arm pass: {} | {}", hand_mode.as_str(), copy_mode.as_str());
681
682            apply_pass_settings(&mut universe.world, arms, hand_mode, *copy_mode);
683            for arm in arms {
684                reset_arm_target_to_pass_baseline(universe, *arm, hand_mode);
685            }
686            settle_world(universe, SETTLE_TICKS_PER_POSE);
687
688            let baselines: Vec<(ArmSide, ArmPassBaseline)> = arms
689                .iter()
690                .map(|arm| {
691                    (
692                        arm.side,
693                        ArmPassBaseline {
694                            hand_world_pos: arm.rest_hand_world_pos,
695                            target_world_rot: world_rot(&universe.world, arm.target),
696                            forearm_axis_world: forearm_axis_world(
697                                &universe.world,
698                                arm.lower_arm,
699                                arm.hand,
700                            ),
701                        },
702                    )
703                })
704                .collect();
705
706            for pose in &poses {
707                for arm in arms {
708                    let baseline = baselines
709                        .iter()
710                        .find(|(side, _)| *side == arm.side)
711                        .map(|(_, b)| *b)
712                        .unwrap();
713                    let twist_rad = match arm.side {
714                        ArmSide::Left => pose.left_twist_rad,
715                        ArmSide::Right => pose.right_twist_rad,
716                    };
717                    set_arm_target_world_pose(
718                        universe,
719                        *arm,
720                        baseline.hand_world_pos,
721                        quat_mul(
722                            quat_from_axis_angle(baseline.forearm_axis_world, twist_rad),
723                            baseline.target_world_rot,
724                        ),
725                    );
726                }
727                settle_world(universe, SETTLE_TICKS_PER_POSE);
728
729                let left = arms
730                    .iter()
731                    .find(|arm| matches!(arm.side, ArmSide::Left))
732                    .copied()
733                    .map(|arm| sample_arm_pose(&universe.world, arm))
734                    .expect("left arm sample missing");
735                let right = arms
736                    .iter()
737                    .find(|arm| matches!(arm.side, ArmSide::Right))
738                    .copied()
739                    .map(|arm| sample_arm_pose(&universe.world, arm))
740                    .expect("right arm sample missing");
741
742                println!("\npose: {}", pose.name);
743                println!("  {}", pose.description);
744                print_arm_pose_sample(&universe.world, ArmSide::Left, &left);
745                print_arm_pose_sample(&universe.world, ArmSide::Right, &right);
746
747                if pose.name == "left_palm_down" || pose.name == "right_palm_down" {
748                    continue;
749                }
750            }
751
752            let left_down = sample_signature_for_pose(
753                universe,
754                arms,
755                &baselines,
756                "left_palm_down",
757                -WRIST_TWIST_RAD,
758                0.0,
759            );
760            let right_down = sample_signature_for_pose(
761                universe,
762                arms,
763                &baselines,
764                "right_palm_down",
765                0.0,
766                WRIST_TWIST_RAD,
767            );
768            let left_up = sample_signature_for_pose(
769                universe,
770                arms,
771                &baselines,
772                "left_palm_up",
773                WRIST_TWIST_RAD,
774                0.0,
775            );
776            let right_up = sample_signature_for_pose(
777                universe,
778                arms,
779                &baselines,
780                "right_palm_up",
781                0.0,
782                -WRIST_TWIST_RAD,
783            );
784
785            println!("\nmirror summary");
786            print_mirror_compare("palm_down", left_down, right_down);
787            print_mirror_compare("palm_up", left_up, right_up);
788        }
789    }
790}
791
792fn sample_signature_for_pose(
793    universe: &mut engine::Universe,
794    arms: &[ArmChainRuntime],
795    baselines: &[(ArmSide, ArmPassBaseline)],
796    _pose_name: &str,
797    left_twist_rad: f32,
798    right_twist_rad: f32,
799) -> MirrorSignature {
800    for arm in arms {
801        let baseline = baselines
802            .iter()
803            .find(|(side, _)| *side == arm.side)
804            .map(|(_, b)| *b)
805            .unwrap();
806        let twist = match arm.side {
807            ArmSide::Left => left_twist_rad,
808            ArmSide::Right => right_twist_rad,
809        };
810        set_arm_target_world_pose(
811            universe,
812            *arm,
813            baseline.hand_world_pos,
814            quat_mul(
815                quat_from_axis_angle(baseline.forearm_axis_world, twist),
816                baseline.target_world_rot,
817            ),
818        );
819    }
820    settle_world(universe, SETTLE_TICKS_PER_POSE);
821
822    let active_arm = if left_twist_rad.abs() > right_twist_rad.abs() {
823        arms.iter()
824            .find(|arm| matches!(arm.side, ArmSide::Left))
825            .copied()
826            .unwrap()
827    } else {
828        arms.iter()
829            .find(|arm| matches!(arm.side, ArmSide::Right))
830            .copied()
831            .unwrap()
832    };
833    let sample = sample_arm_pose(&universe.world, active_arm);
834    MirrorSignature {
835        lower_to_hand_deg: sample.lower_to_hand_deg,
836        lower_to_target_deg: sample.lower_to_target_deg,
837        hand_to_target_deg: sample.hand_to_target_deg,
838    }
839}
840
841fn apply_pass_settings(
842    world: &mut World,
843    arms: &[ArmChainRuntime],
844    hand_mode: HandOffsetMode,
845    copy_mode: CopyEndRotationMode,
846) {
847    for arm in arms {
848        if let Some(tc) = world.get_component_by_id_as_mut::<TransformComponent>(arm.target) {
849            tc.transform.rotation = match hand_mode {
850                HandOffsetMode::Authored => arm.authored_target_local_rotation,
851                HandOffsetMode::Neutralized => ident_quat(),
852            };
853            tc.transform.recompute_model();
854        }
855        if let Some(ik) = world.get_component_by_id_as_mut::<IKChainComponent>(arm.ik_chain_id) {
856            if let IKSolver::TwoBoneIK {
857                root_joint_id,
858                mid_joint_id,
859                pole_direction,
860                copy_end_rotation: enabled,
861            } = &mut ik.solver
862            {
863                let _ = (root_joint_id, mid_joint_id, pole_direction);
864                *enabled = match copy_mode {
865                    CopyEndRotationMode::SolverDefault => arm.original_copy_end_rotation,
866                    CopyEndRotationMode::ForcedOff => false,
867                };
868            }
869        }
870    }
871}
872
873fn reset_arm_target_to_pass_baseline(
874    universe: &mut engine::Universe,
875    arm: ArmChainRuntime,
876    hand_mode: HandOffsetMode,
877) {
878    let world_rot = match hand_mode {
879        HandOffsetMode::Authored => world_rot(&universe.world, arm.target),
880        HandOffsetMode::Neutralized => world_rot(&universe.world, arm.target),
881    };
882    set_arm_target_world_pose(universe, arm, arm.rest_hand_world_pos, world_rot);
883}
884
885fn set_arm_target_world_pose(
886    universe: &mut engine::Universe,
887    arm: ArmChainRuntime,
888    desired_world_pos: [f32; 3],
889    desired_world_rot: [f32; 4],
890) {
891    let (local_translation, local_rotation, local_scale) = world_pose_to_local(
892        &universe.world,
893        arm.target,
894        desired_world_pos,
895        desired_world_rot,
896    );
897    set_local_transform(
898        &mut universe.command_queue,
899        arm.target,
900        local_translation,
901        local_rotation,
902        local_scale,
903    );
904}
905
906fn sample_arm_pose(world: &World, arm: ArmChainRuntime) -> ArmPoseSample {
907    let upper_arm = sample_node_rotation(world, arm.upper_arm);
908    let lower_arm = sample_node_rotation(world, arm.lower_arm);
909    let hand = sample_node_rotation(world, arm.hand);
910    let target = sample_node_rotation(world, arm.target);
911    ArmPoseSample {
912        upper_arm,
913        lower_arm,
914        hand,
915        target,
916        lower_to_hand_deg: quat_delta_deg(lower_arm.world, hand.world),
917        lower_to_target_deg: quat_delta_deg(lower_arm.world, target.world),
918        hand_to_target_deg: quat_delta_deg(hand.world, target.world),
919    }
920}
921
922fn sample_node_rotation(world: &World, id: ComponentId) -> NodeRotationSample {
923    NodeRotationSample {
924        world: world_rot(world, id),
925        local: local_rot(world, id),
926    }
927}
928
929fn print_arm_pose_sample(world: &World, side: ArmSide, sample: &ArmPoseSample) {
930    println!("  [{}]", side.as_str());
931    print_node_rot("upper_arm", sample.upper_arm);
932    print_node_rot("lower_arm", sample.lower_arm);
933    print_node_rot("hand", sample.hand);
934    print_node_rot("target", sample.target);
935    println!(
936        "    deltas: lower→hand={:>6.2}°  lower→target={:>6.2}°  hand→target={:>6.2}°",
937        sample.lower_to_hand_deg, sample.lower_to_target_deg, sample.hand_to_target_deg
938    );
939    let _ = world;
940}
941
942fn print_node_rot(label: &str, sample: NodeRotationSample) {
943    println!(
944        "    {:<9} world={}  local={}",
945        label,
946        fmt_quat(sample.world),
947        fmt_quat(sample.local)
948    );
949}
950
951fn print_mirror_compare(name: &str, left: MirrorSignature, right: MirrorSignature) {
952    println!(
953        "  {:<10} lower→hand L/R={:>6.2}°/{:>6.2}°  lower→target L/R={:>6.2}°/{:>6.2}°  hand→target L/R={:>6.2}°/{:>6.2}°",
954        name,
955        left.lower_to_hand_deg,
956        right.lower_to_hand_deg,
957        left.lower_to_target_deg,
958        right.lower_to_target_deg,
959        left.hand_to_target_deg,
960        right.hand_to_target_deg,
961    );
962}
963
964fn upward_chain_labels(
965    world: &World,
966    start: ComponentId,
967    stop_at: Option<ComponentId>,
968    max_hops: usize,
969) -> Vec<String> {
970    let mut out = vec![describe_component(world, start)];
971    let mut cur = start;
972    for _ in 0..max_hops {
973        let Some(parent) = world.parent_of(cur) else {
974            break;
975        };
976        out.push(describe_component(world, parent));
977        if Some(parent) == stop_at {
978            break;
979        }
980        cur = parent;
981    }
982    out
983}
984
985fn skipped_chain_labels(
986    world: &World,
987    hand: ComponentId,
988    lower: ComponentId,
989    upper: ComponentId,
990) -> Vec<String> {
991    let mut skipped = Vec::new();
992    let mut cur = hand;
993    let mut seen_lower = false;
994    for _ in 0..8 {
995        let Some(parent) = world.parent_of(cur) else {
996            break;
997        };
998        if parent == lower {
999            seen_lower = true;
1000        } else if parent == upper {
1001            break;
1002        } else if seen_lower {
1003            skipped.push(describe_component(world, parent));
1004        }
1005        cur = parent;
1006    }
1007    skipped
1008}
1009
1010fn describe_component(world: &World, id: ComponentId) -> String {
1011    let label = world
1012        .component_label(id)
1013        .or_else(|| world.component_name(id));
1014    format!("{}({id:?})", label.unwrap_or("?"))
1015}
1016
1017fn set_local_transform(
1018    emit: &mut dyn SignalEmitter,
1019    id: ComponentId,
1020    translation: [f32; 3],
1021    rotation_quat_xyzw: [f32; 4],
1022    scale: [f32; 3],
1023) {
1024    emit.push_intent_now(
1025        id,
1026        IntentValue::UpdateTransform {
1027            component_ids: vec![id],
1028            translation,
1029            rotation_quat_xyzw,
1030            scale,
1031        },
1032    );
1033}
1034
1035fn world_pose_to_local(
1036    world: &World,
1037    id: ComponentId,
1038    desired_world_pos: [f32; 3],
1039    desired_world_rot: [f32; 4],
1040) -> ([f32; 3], [f32; 4], [f32; 3]) {
1041    let scale = world
1042        .get_component_by_id_as::<TransformComponent>(id)
1043        .map(|t| t.transform.scale)
1044        .unwrap_or([1.0, 1.0, 1.0]);
1045
1046    let parent_world_mat = parent_world_matrix(world, id).unwrap_or(mat4_identity());
1047    let parent_world_rot = parent_world_rotation(world, id).unwrap_or(ident_quat());
1048    let inv_parent = mat4_inverse(parent_world_mat).unwrap_or(mat4_identity());
1049    let local_pos4 = mat4_mul_vec4(
1050        inv_parent,
1051        [
1052            desired_world_pos[0],
1053            desired_world_pos[1],
1054            desired_world_pos[2],
1055            1.0,
1056        ],
1057    );
1058    let local_rot = quat_mul(quat_conjugate(parent_world_rot), desired_world_rot);
1059    (
1060        [local_pos4[0], local_pos4[1], local_pos4[2]],
1061        local_rot,
1062        scale,
1063    )
1064}
1065
1066fn parent_world_matrix(world: &World, id: ComponentId) -> Option<[[f32; 4]; 4]> {
1067    let parent = world.parent_of(id)?;
1068    world
1069        .get_component_by_id_as::<TransformComponent>(parent)
1070        .map(|t| t.transform.matrix_world)
1071}
1072
1073fn parent_world_rotation(world: &World, id: ComponentId) -> Option<[f32; 4]> {
1074    let parent = world.parent_of(id)?;
1075    world
1076        .get_component_by_id_as::<TransformComponent>(parent)
1077        .map(|t| mat_to_quat(t.transform.matrix_world))
1078}
1079
1080fn forearm_axis_world(world: &World, lower_arm: ComponentId, hand: ComponentId) -> [f32; 3] {
1081    let axis = vec3_sub(world_pos(world, hand), world_pos(world, lower_arm));
1082    if vec3_len(axis) > 1e-6 {
1083        vec3_normalize(axis)
1084    } else {
1085        [0.0, 0.0, 1.0]
1086    }
1087}
1088
1089fn quat_delta_deg(from: [f32; 4], to: [f32; 4]) -> f32 {
1090    let (_, angle) = quat_to_axis_angle(quat_mul(quat_conjugate(from), to));
1091    angle.abs().to_degrees()
1092}
1093
1094fn fmt_quat(q: [f32; 4]) -> String {
1095    format!("[{:+.3},{:+.3},{:+.3},{:+.3}]", q[0], q[1], q[2], q[3])
1096}
1097
1098fn ident_quat() -> [f32; 4] {
1099    [0.0, 0.0, 0.0, 1.0]
1100}
1101
1102fn drain(universe: &mut engine::Universe) {
1103    universe.systems.process_commands(
1104        &mut universe.world,
1105        &mut universe.visuals,
1106        &mut universe.render_assets,
1107        &mut universe.command_queue,
1108    );
1109}
1110
1111fn tick_gltf(universe: &mut engine::Universe) {
1112    let systems = &mut universe.systems;
1113    systems.gltf.tick_with_queue(
1114        &mut universe.world,
1115        &mut universe.visuals,
1116        &mut systems.skinned_mesh,
1117        &mut universe.command_queue,
1118        0.0,
1119    );
1120}
1121
1122fn settle_world(universe: &mut engine::Universe, ticks: usize) {
1123    let input = InputState::default();
1124    for _ in 0..ticks {
1125        universe.systems.tick(
1126            &mut universe.world,
1127            &mut universe.visuals,
1128            &mut universe.render_assets,
1129            &input,
1130            &mut universe.command_queue,
1131            1.0 / 60.0,
1132        );
1133        drain(universe);
1134    }
1135}
1136
1137fn find_components_by_name(world: &World, name: &str) -> Vec<ComponentId> {
1138    world
1139        .all_components()
1140        .filter(|&id| world.component_label(id).map_or(false, |n| n == name))
1141        .collect()
1142}
1143
1144fn is_descendant_of(world: &World, child: ComponentId, ancestor: ComponentId) -> bool {
1145    let mut cur = child;
1146    for _ in 0..64 {
1147        let Some(parent) = world.parent_of(cur) else {
1148            return false;
1149        };
1150        if parent == ancestor {
1151            return true;
1152        }
1153        cur = parent;
1154    }
1155    false
1156}
1157
1158fn world_pos(world: &World, id: ComponentId) -> [f32; 3] {
1159    world
1160        .get_component_by_id_as::<TransformComponent>(id)
1161        .map(|t| {
1162            let m = t.transform.matrix_world;
1163            [m[3][0], m[3][1], m[3][2]]
1164        })
1165        .unwrap_or([0.0; 3])
1166}
1167
1168fn world_rot(world: &World, id: ComponentId) -> [f32; 4] {
1169    world
1170        .get_component_by_id_as::<TransformComponent>(id)
1171        .map(|t| mat_to_quat(t.transform.matrix_world))
1172        .unwrap_or(ident_quat())
1173}
1174
1175fn local_rot(world: &World, id: ComponentId) -> [f32; 4] {
1176    world
1177        .get_component_by_id_as::<TransformComponent>(id)
1178        .map(|t| t.transform.rotation)
1179        .unwrap_or(ident_quat())
1180}
1181
1182fn world_yz(world: &World, id: ComponentId) -> [f32; 2] {
1183    let p = world_pos(world, id);
1184    [p[1], p[2]]
1185}
1186
1187fn vec3_dist(a: [f32; 3], b: [f32; 3]) -> f32 {
1188    let d = vec3_sub(a, b);
1189    vec3_len(d)
1190}
1191
1192fn segment_lengths(chain: &[ComponentId], world: &World) -> Vec<f32> {
1193    chain
1194        .windows(2)
1195        .map(|pair| vec3_dist(world_pos(world, pair[0]), world_pos(world, pair[1])))
1196        .collect()
1197}
1198
1199fn head_target_offset_in_target_local(world: &World, avc_id: ComponentId) -> [f32; 3] {
1200    let avc = world
1201        .get_component_by_id_as::<AvatarControlComponent>(avc_id)
1202        .expect("avc missing");
1203    let mut eye_offset = [0.0, 0.0, 0.0];
1204    for &ch in world.children_of(avc_id) {
1205        let is_t = world
1206            .get_component_by_id_as::<TransformComponent>(ch)
1207            .is_some();
1208        if !is_t {
1209            continue;
1210        }
1211        let wraps_cam = world.children_of(ch).iter().any(|&gc| {
1212            world
1213                .get_component_by_id_as::<mittens_engine::engine::ecs::component::Camera3DComponent>(gc)
1214                .is_some()
1215                || world
1216                    .get_component_by_id_as::<mittens_engine::engine::ecs::component::CameraXRComponent>(gc)
1217                    .is_some()
1218        });
1219        if wraps_cam {
1220            eye_offset = world
1221                .get_component_by_id_as::<TransformComponent>(ch)
1222                .unwrap()
1223                .transform
1224                .translation;
1225            break;
1226        }
1227    }
1228    let offset_yaw = if avc.forward_plus_z {
1229        0.0
1230    } else {
1231        std::f32::consts::PI
1232    };
1233    let neg_eye = [-eye_offset[0], -eye_offset[1], -eye_offset[2]];
1234    quat_rotate_vec3(quat_rotation_y(offset_yaw), neg_eye)
1235}