1use 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 ©_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}