Skip to main content

proof_engine/editor/
anim_retarget.rs

1
2//! Animation retargeting system — skeleton mapping, IK solving, motion adaptation.
3
4use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
5use std::collections::HashMap;
6
7// ---------------------------------------------------------------------------
8// Skeleton
9// ---------------------------------------------------------------------------
10
11#[derive(Debug, Clone)]
12pub struct Bone {
13    pub name: String,
14    pub parent: Option<usize>,
15    pub bind_pose_local: Mat4,
16    pub inverse_bind_pose: Mat4,
17    pub length: f32,
18    pub flags: BoneFlags,
19}
20
21#[derive(Debug, Clone, Copy, Default)]
22pub struct BoneFlags {
23    pub is_root: bool,
24    pub is_leaf: bool,
25    pub is_twist: bool,
26    pub is_ik_target: bool,
27    pub has_physics: bool,
28    pub no_export: bool,
29}
30
31#[derive(Debug, Clone)]
32pub struct Skeleton {
33    pub name: String,
34    pub bones: Vec<Bone>,
35    pub root_indices: Vec<usize>,
36}
37
38impl Skeleton {
39    pub fn new(name: impl Into<String>) -> Self {
40        Self { name: name.into(), bones: Vec::new(), root_indices: Vec::new() }
41    }
42
43    pub fn add_bone(&mut self, bone: Bone) -> usize {
44        let i = self.bones.len();
45        if bone.parent.is_none() {
46            self.root_indices.push(i);
47        }
48        self.bones.push(bone);
49        i
50    }
51
52    pub fn find_bone(&self, name: &str) -> Option<usize> {
53        self.bones.iter().position(|b| b.name == name)
54    }
55
56    pub fn bone_count(&self) -> usize {
57        self.bones.len()
58    }
59
60    pub fn children_of(&self, idx: usize) -> Vec<usize> {
61        self.bones.iter().enumerate()
62            .filter(|(_, b)| b.parent == Some(idx))
63            .map(|(i, _)| i)
64            .collect()
65    }
66
67    pub fn chain_to_root(&self, start: usize) -> Vec<usize> {
68        let mut chain = Vec::new();
69        let mut current = Some(start);
70        while let Some(i) = current {
71            chain.push(i);
72            current = self.bones[i].parent;
73        }
74        chain
75    }
76
77    pub fn world_transform(&self, idx: usize, local_poses: &[Mat4]) -> Mat4 {
78        let chain = self.chain_to_root(idx);
79        let mut mat = Mat4::IDENTITY;
80        for &i in chain.iter().rev() {
81            mat = mat * local_poses[i];
82        }
83        mat
84    }
85
86    /// Build a simple biped skeleton for testing.
87    pub fn build_biped() -> Self {
88        let mut skel = Skeleton::new("Biped");
89        let root = skel.add_bone(Bone {
90            name: "Root".into(), parent: None,
91            bind_pose_local: Mat4::IDENTITY, inverse_bind_pose: Mat4::IDENTITY,
92            length: 0.1, flags: BoneFlags { is_root: true, ..Default::default() },
93        });
94        let hips = skel.add_bone(Bone {
95            name: "Hips".into(), parent: Some(root),
96            bind_pose_local: Mat4::from_translation(Vec3::new(0.0, 1.0, 0.0)),
97            inverse_bind_pose: Mat4::from_translation(Vec3::new(0.0, -1.0, 0.0)),
98            length: 0.2, flags: Default::default(),
99        });
100        let spine = skel.add_bone(Bone {
101            name: "Spine".into(), parent: Some(hips),
102            bind_pose_local: Mat4::from_translation(Vec3::new(0.0, 0.25, 0.0)),
103            inverse_bind_pose: Mat4::from_translation(Vec3::new(0.0, -0.25, 0.0)),
104            length: 0.25, flags: Default::default(),
105        });
106        let chest = skel.add_bone(Bone {
107            name: "Chest".into(), parent: Some(spine),
108            bind_pose_local: Mat4::from_translation(Vec3::new(0.0, 0.25, 0.0)),
109            inverse_bind_pose: Mat4::from_translation(Vec3::new(0.0, -0.25, 0.0)),
110            length: 0.25, flags: Default::default(),
111        });
112        let neck = skel.add_bone(Bone {
113            name: "Neck".into(), parent: Some(chest),
114            bind_pose_local: Mat4::from_translation(Vec3::new(0.0, 0.25, 0.0)),
115            inverse_bind_pose: Mat4::from_translation(Vec3::new(0.0, -0.25, 0.0)),
116            length: 0.15, flags: Default::default(),
117        });
118        let _head = skel.add_bone(Bone {
119            name: "Head".into(), parent: Some(neck),
120            bind_pose_local: Mat4::from_translation(Vec3::new(0.0, 0.15, 0.0)),
121            inverse_bind_pose: Mat4::from_translation(Vec3::new(0.0, -0.15, 0.0)),
122            length: 0.2, flags: BoneFlags { is_leaf: false, ..Default::default() },
123        });
124        // Arms
125        for (side, x) in [("L", 0.2_f32), ("R", -0.2)] {
126            let shoulder = skel.add_bone(Bone {
127                name: format!("{}_Shoulder", side), parent: Some(chest),
128                bind_pose_local: Mat4::from_translation(Vec3::new(x, 0.2, 0.0)),
129                inverse_bind_pose: Mat4::from_translation(Vec3::new(-x, -0.2, 0.0)),
130                length: 0.15, flags: Default::default(),
131            });
132            let upper_arm = skel.add_bone(Bone {
133                name: format!("{}_UpperArm", side), parent: Some(shoulder),
134                bind_pose_local: Mat4::from_translation(Vec3::new(x * 1.5, 0.0, 0.0)),
135                inverse_bind_pose: Mat4::from_translation(Vec3::new(-x * 1.5, 0.0, 0.0)),
136                length: 0.3, flags: Default::default(),
137            });
138            let lower_arm = skel.add_bone(Bone {
139                name: format!("{}_LowerArm", side), parent: Some(upper_arm),
140                bind_pose_local: Mat4::from_translation(Vec3::new(x * 1.5, 0.0, 0.0)),
141                inverse_bind_pose: Mat4::from_translation(Vec3::new(-x * 1.5, 0.0, 0.0)),
142                length: 0.28, flags: Default::default(),
143            });
144            let _hand = skel.add_bone(Bone {
145                name: format!("{}_Hand", side), parent: Some(lower_arm),
146                bind_pose_local: Mat4::from_translation(Vec3::new(x * 1.2, 0.0, 0.0)),
147                inverse_bind_pose: Mat4::from_translation(Vec3::new(-x * 1.2, 0.0, 0.0)),
148                length: 0.1, flags: BoneFlags { is_ik_target: true, ..Default::default() },
149            });
150        }
151        // Legs
152        for (side, x) in [("L", 0.1_f32), ("R", -0.1)] {
153            let upper_leg = skel.add_bone(Bone {
154                name: format!("{}_UpperLeg", side), parent: Some(hips),
155                bind_pose_local: Mat4::from_translation(Vec3::new(x, -0.1, 0.0)),
156                inverse_bind_pose: Mat4::from_translation(Vec3::new(-x, 0.1, 0.0)),
157                length: 0.45, flags: Default::default(),
158            });
159            let lower_leg = skel.add_bone(Bone {
160                name: format!("{}_LowerLeg", side), parent: Some(upper_leg),
161                bind_pose_local: Mat4::from_translation(Vec3::new(0.0, -0.45, 0.0)),
162                inverse_bind_pose: Mat4::from_translation(Vec3::new(0.0, 0.45, 0.0)),
163                length: 0.42, flags: Default::default(),
164            });
165            let _foot = skel.add_bone(Bone {
166                name: format!("{}_Foot", side), parent: Some(lower_leg),
167                bind_pose_local: Mat4::from_translation(Vec3::new(0.0, -0.42, 0.0)),
168                inverse_bind_pose: Mat4::from_translation(Vec3::new(0.0, 0.42, 0.0)),
169                length: 0.15, flags: BoneFlags { is_ik_target: true, ..Default::default() },
170            });
171        }
172        skel
173    }
174}
175
176// ---------------------------------------------------------------------------
177// Pose
178// ---------------------------------------------------------------------------
179
180#[derive(Debug, Clone)]
181pub struct Pose {
182    pub local_transforms: Vec<Mat4>,
183    pub bone_count: usize,
184}
185
186impl Pose {
187    pub fn bind_pose(skel: &Skeleton) -> Self {
188        Self {
189            local_transforms: skel.bones.iter().map(|b| b.bind_pose_local).collect(),
190            bone_count: skel.bones.len(),
191        }
192    }
193
194    pub fn identity(bone_count: usize) -> Self {
195        Self {
196            local_transforms: vec![Mat4::IDENTITY; bone_count],
197            bone_count,
198        }
199    }
200
201    pub fn lerp(&self, other: &Pose, t: f32) -> Pose {
202        assert_eq!(self.bone_count, other.bone_count);
203        let local_transforms = self.local_transforms.iter().zip(other.local_transforms.iter())
204            .map(|(a, b)| {
205                let (sa, qa, ta) = decompose_mat4(*a);
206                let (sb, qb, tb) = decompose_mat4(*b);
207                let s = sa.lerp(sb, t);
208                let q = qa.slerp(qb, t);
209                let tr = ta.lerp(tb, t);
210                Mat4::from_scale_rotation_translation(s, q, tr)
211            })
212            .collect();
213        Pose { local_transforms, bone_count: self.bone_count }
214    }
215}
216
217fn decompose_mat4(m: Mat4) -> (Vec3, Quat, Vec3) {
218    let translation = m.w_axis.truncate();
219    let sx = m.x_axis.truncate().length();
220    let sy = m.y_axis.truncate().length();
221    let sz = m.z_axis.truncate().length();
222    let scale = Vec3::new(sx, sy, sz);
223    let rot_mat = Mat4::from_cols(
224        (m.x_axis.truncate() / sx).extend(0.0),
225        (m.y_axis.truncate() / sy).extend(0.0),
226        (m.z_axis.truncate() / sz).extend(0.0),
227        Vec4::W,
228    );
229    let rotation = Quat::from_mat4(&rot_mat);
230    (scale, rotation, translation)
231}
232
233// ---------------------------------------------------------------------------
234// Bone mapping
235// ---------------------------------------------------------------------------
236
237#[derive(Debug, Clone, Copy, PartialEq)]
238pub enum MappingMode {
239    Exact,          // bones with same name
240    HumanoidRig,    // map via standard biped slots
241    Manual,         // explicit mappings
242    AutoLearned,    // ML-based (placeholder)
243}
244
245#[derive(Debug, Clone, Copy, PartialEq)]
246pub enum HumanoidSlot {
247    Hips, Spine, Chest, UpperChest, Neck, Head,
248    LeftShoulder, LeftUpperArm, LeftLowerArm, LeftHand,
249    RightShoulder, RightUpperArm, RightLowerArm, RightHand,
250    LeftUpperLeg, LeftLowerLeg, LeftFoot, LeftToes,
251    RightUpperLeg, RightLowerLeg, RightFoot, RightToes,
252    LeftThumbProximal, LeftThumbIntermediate, LeftThumbDistal,
253    RightThumbProximal, RightThumbIntermediate, RightThumbDistal,
254}
255
256impl HumanoidSlot {
257    pub fn label(self) -> &'static str {
258        match self {
259            HumanoidSlot::Hips => "Hips",
260            HumanoidSlot::Spine => "Spine",
261            HumanoidSlot::Chest => "Chest",
262            HumanoidSlot::UpperChest => "Upper Chest",
263            HumanoidSlot::Neck => "Neck",
264            HumanoidSlot::Head => "Head",
265            HumanoidSlot::LeftUpperArm => "Left Upper Arm",
266            HumanoidSlot::LeftLowerArm => "Left Lower Arm",
267            HumanoidSlot::LeftHand => "Left Hand",
268            HumanoidSlot::RightUpperArm => "Right Upper Arm",
269            HumanoidSlot::RightLowerArm => "Right Lower Arm",
270            HumanoidSlot::RightHand => "Right Hand",
271            HumanoidSlot::LeftUpperLeg => "Left Upper Leg",
272            HumanoidSlot::LeftLowerLeg => "Left Lower Leg",
273            HumanoidSlot::LeftFoot => "Left Foot",
274            HumanoidSlot::RightUpperLeg => "Right Upper Leg",
275            HumanoidSlot::RightLowerLeg => "Right Lower Leg",
276            HumanoidSlot::RightFoot => "Right Foot",
277            _ => "Other",
278        }
279    }
280}
281
282#[derive(Debug, Clone)]
283pub struct BoneMapping {
284    pub source_bone: String,
285    pub target_bone: String,
286    pub rotation_offset: Quat,
287    pub scale_factor: f32,
288    pub position_correction: Vec3,
289    pub mirror: bool,
290    pub invert_axes: [bool; 3],
291}
292
293impl BoneMapping {
294    pub fn direct(source: impl Into<String>, target: impl Into<String>) -> Self {
295        Self {
296            source_bone: source.into(),
297            target_bone: target.into(),
298            rotation_offset: Quat::IDENTITY,
299            scale_factor: 1.0,
300            position_correction: Vec3::ZERO,
301            mirror: false,
302            invert_axes: [false; 3],
303        }
304    }
305}
306
307// ---------------------------------------------------------------------------
308// Retargeting settings
309// ---------------------------------------------------------------------------
310
311#[derive(Debug, Clone, Copy, PartialEq)]
312pub enum HeightMatchMode {
313    None, ScaleUniform, ScaleLegs, IKAdapt,
314}
315
316#[derive(Debug, Clone)]
317pub struct RetargetingSettings {
318    pub mode: MappingMode,
319    pub height_match: HeightMatchMode,
320    pub preserve_foot_contact: bool,
321    pub preserve_hand_contact: bool,
322    pub copy_root_motion: bool,
323    pub root_motion_scale: f32,
324    pub compensate_hip_position: bool,
325    pub ik_solve_hands: bool,
326    pub ik_solve_feet: bool,
327    pub ik_iterations: u32,
328    pub ik_tolerance: f32,
329    pub global_scale: f32,
330}
331
332impl Default for RetargetingSettings {
333    fn default() -> Self {
334        Self {
335            mode: MappingMode::HumanoidRig,
336            height_match: HeightMatchMode::ScaleLegs,
337            preserve_foot_contact: true,
338            preserve_hand_contact: false,
339            copy_root_motion: true,
340            root_motion_scale: 1.0,
341            compensate_hip_position: true,
342            ik_solve_hands: false,
343            ik_solve_feet: true,
344            ik_iterations: 20,
345            ik_tolerance: 0.001,
346            global_scale: 1.0,
347        }
348    }
349}
350
351// ---------------------------------------------------------------------------
352// IK solver (FABRIK)
353// ---------------------------------------------------------------------------
354
355#[derive(Debug, Clone)]
356pub struct IkChain {
357    pub bone_indices: Vec<usize>,
358    pub positions: Vec<Vec3>,
359    pub lengths: Vec<f32>,
360    pub target: Vec3,
361    pub pole_target: Option<Vec3>,
362    pub constraints: Vec<IkConstraint>,
363}
364
365#[derive(Debug, Clone, Copy)]
366pub struct IkConstraint {
367    pub min_angle: f32,
368    pub max_angle: f32,
369    pub twist_min: f32,
370    pub twist_max: f32,
371}
372
373impl IkChain {
374    pub fn new(bone_indices: Vec<usize>, positions: Vec<Vec3>, lengths: Vec<f32>) -> Self {
375        let target = *positions.last().unwrap_or(&Vec3::ZERO);
376        Self {
377            bone_indices, positions, lengths, target,
378            pole_target: None, constraints: Vec::new(),
379        }
380    }
381
382    /// FABRIK iteration.
383    pub fn solve_fabrik(&mut self, max_iter: u32, tolerance: f32) {
384        let n = self.positions.len();
385        if n < 2 { return; }
386        let root = self.positions[0];
387        let total_length: f32 = self.lengths.iter().sum();
388        let dist = self.target.distance(root);
389
390        // Unreachable case: stretch toward target
391        if dist >= total_length {
392            for i in 1..n {
393                let d = (self.target - self.positions[i-1]).normalize();
394                self.positions[i] = self.positions[i-1] + d * self.lengths[i-1];
395            }
396            return;
397        }
398
399        for _ in 0..max_iter {
400            // Forward reaching
401            self.positions[n-1] = self.target;
402            for i in (0..n-1).rev() {
403                let d = (self.positions[i] - self.positions[i+1]).normalize();
404                self.positions[i] = self.positions[i+1] + d * self.lengths[i];
405            }
406            // Backward reaching
407            self.positions[0] = root;
408            for i in 0..n-1 {
409                let d = (self.positions[i+1] - self.positions[i]).normalize();
410                self.positions[i+1] = self.positions[i] + d * self.lengths[i];
411            }
412            // Check convergence
413            if self.positions[n-1].distance(self.target) < tolerance {
414                break;
415            }
416        }
417    }
418
419    /// Returns local rotations for each bone.
420    pub fn compute_rotations(&self, rest_positions: &[Vec3]) -> Vec<Quat> {
421        let n = self.positions.len() - 1;
422        (0..n).map(|i| {
423            let rest_dir = (rest_positions[i+1] - rest_positions[i]).normalize();
424            let solved_dir = (self.positions[i+1] - self.positions[i]).normalize();
425            if rest_dir.dot(solved_dir) > 0.9999 {
426                Quat::IDENTITY
427            } else {
428                Quat::from_rotation_arc(rest_dir, solved_dir)
429            }
430        }).collect()
431    }
432}
433
434// ---------------------------------------------------------------------------
435// Retargeter
436// ---------------------------------------------------------------------------
437
438#[derive(Debug, Clone)]
439pub struct AnimationRetargeter {
440    pub source_skeleton: Skeleton,
441    pub target_skeleton: Skeleton,
442    pub mappings: Vec<BoneMapping>,
443    pub settings: RetargetingSettings,
444    pub humanoid_source: HashMap<String, String>,
445    pub humanoid_target: HashMap<String, String>,
446    pub source_height: f32,
447    pub target_height: f32,
448}
449
450// Use String keys in HashMap since HumanoidSlot doesn't implement Hash easily
451#[derive(Debug, Clone)]
452pub struct AnimRetargeter {
453    pub source_skeleton: Skeleton,
454    pub target_skeleton: Skeleton,
455    pub mappings: Vec<BoneMapping>,
456    pub settings: RetargetingSettings,
457    pub source_height: f32,
458    pub target_height: f32,
459    pub scale_factor: f32,
460}
461
462impl AnimRetargeter {
463    pub fn new(source: Skeleton, target: Skeleton) -> Self {
464        let src_h = 1.8_f32;
465        let tgt_h = 1.8_f32;
466        Self {
467            source_skeleton: source,
468            target_skeleton: target,
469            mappings: Vec::new(),
470            settings: RetargetingSettings::default(),
471            source_height: src_h,
472            target_height: tgt_h,
473            scale_factor: tgt_h / src_h,
474        }
475    }
476
477    pub fn auto_map_by_name(&mut self) {
478        self.mappings.clear();
479        for src_bone in &self.source_skeleton.bones {
480            if let Some(_) = self.target_skeleton.find_bone(&src_bone.name) {
481                self.mappings.push(BoneMapping::direct(&src_bone.name, &src_bone.name));
482            }
483        }
484    }
485
486    pub fn add_mapping(&mut self, mapping: BoneMapping) {
487        self.mappings.retain(|m| m.source_bone != mapping.source_bone);
488        self.mappings.push(mapping);
489    }
490
491    pub fn retarget_pose(&self, source_pose: &Pose) -> Pose {
492        let n = self.target_skeleton.bone_count();
493        let mut result = Pose::bind_pose(&self.target_skeleton);
494
495        for mapping in &self.mappings {
496            let src_idx = self.source_skeleton.find_bone(&mapping.source_bone);
497            let tgt_idx = self.target_skeleton.find_bone(&mapping.target_bone);
498            if let (Some(si), Some(ti)) = (src_idx, tgt_idx) {
499                let src_mat = source_pose.local_transforms[si];
500                let (scale, rot, trans) = decompose_mat4(src_mat);
501                // Apply rotation offset and correction
502                let rot = rot * mapping.rotation_offset;
503                let trans = (trans + mapping.position_correction) * mapping.scale_factor * self.scale_factor;
504                result.local_transforms[ti] = Mat4::from_scale_rotation_translation(scale, rot, trans);
505            }
506        }
507        result
508    }
509
510    pub fn unmapped_source_bones(&self) -> Vec<&str> {
511        self.source_skeleton.bones.iter()
512            .filter(|b| !self.mappings.iter().any(|m| m.source_bone == b.name))
513            .map(|b| b.name.as_str())
514            .collect()
515    }
516
517    pub fn unmapped_target_bones(&self) -> Vec<&str> {
518        self.target_skeleton.bones.iter()
519            .filter(|b| !self.mappings.iter().any(|m| m.target_bone == b.name))
520            .map(|b| b.name.as_str())
521            .collect()
522    }
523}
524
525// ---------------------------------------------------------------------------
526// Animation clip
527// ---------------------------------------------------------------------------
528
529#[derive(Debug, Clone)]
530pub struct AnimationTrack {
531    pub bone_name: String,
532    pub times: Vec<f32>,
533    pub translations: Vec<Vec3>,
534    pub rotations: Vec<Quat>,
535    pub scales: Vec<Vec3>,
536}
537
538impl AnimationTrack {
539    pub fn new(bone_name: impl Into<String>) -> Self {
540        Self {
541            bone_name: bone_name.into(),
542            times: Vec::new(),
543            translations: Vec::new(),
544            rotations: Vec::new(),
545            scales: Vec::new(),
546        }
547    }
548
549    pub fn add_keyframe(&mut self, time: f32, translation: Vec3, rotation: Quat, scale: Vec3) {
550        let i = self.times.partition_point(|&t| t <= time);
551        self.times.insert(i, time);
552        self.translations.insert(i, translation);
553        self.rotations.insert(i, rotation);
554        self.scales.insert(i, scale);
555    }
556
557    pub fn sample(&self, time: f32) -> Mat4 {
558        if self.times.is_empty() { return Mat4::IDENTITY; }
559        if self.times.len() == 1 {
560            return Mat4::from_scale_rotation_translation(self.scales[0], self.rotations[0], self.translations[0]);
561        }
562        let i = self.times.partition_point(|&t| t <= time);
563        if i == 0 {
564            return Mat4::from_scale_rotation_translation(self.scales[0], self.rotations[0], self.translations[0]);
565        }
566        if i >= self.times.len() {
567            let last = self.times.len() - 1;
568            return Mat4::from_scale_rotation_translation(self.scales[last], self.rotations[last], self.translations[last]);
569        }
570        let t0 = self.times[i-1];
571        let t1 = self.times[i];
572        let u = ((time - t0) / (t1 - t0)).clamp(0.0, 1.0);
573        let translation = self.translations[i-1].lerp(self.translations[i], u);
574        let rotation = self.rotations[i-1].slerp(self.rotations[i], u);
575        let scale = self.scales[i-1].lerp(self.scales[i], u);
576        Mat4::from_scale_rotation_translation(scale, rotation, translation)
577    }
578
579    pub fn duration(&self) -> f32 {
580        self.times.last().copied().unwrap_or(0.0)
581    }
582}
583
584#[derive(Debug, Clone)]
585pub struct AnimationClip {
586    pub name: String,
587    pub duration: f32,
588    pub frame_rate: f32,
589    pub tracks: Vec<AnimationTrack>,
590    pub loop_mode: LoopMode,
591    pub root_motion: bool,
592}
593
594#[derive(Debug, Clone, Copy, PartialEq)]
595pub enum LoopMode { Once, Loop, PingPong, ClampForever }
596
597impl AnimationClip {
598    pub fn new(name: impl Into<String>, duration: f32, fps: f32) -> Self {
599        Self {
600            name: name.into(),
601            duration,
602            frame_rate: fps,
603            tracks: Vec::new(),
604            loop_mode: LoopMode::Loop,
605            root_motion: false,
606        }
607    }
608
609    pub fn sample_pose(&self, time: f32, skeleton: &Skeleton) -> Pose {
610        let mut pose = Pose::bind_pose(skeleton);
611        for track in &self.tracks {
612            if let Some(idx) = skeleton.find_bone(&track.bone_name) {
613                pose.local_transforms[idx] = track.sample(time);
614            }
615        }
616        pose
617    }
618
619    pub fn frame_count(&self) -> u32 {
620        (self.duration * self.frame_rate) as u32
621    }
622
623    pub fn normalized_time(&self, time: f32) -> f32 {
624        match self.loop_mode {
625            LoopMode::Once => (time / self.duration).min(1.0),
626            LoopMode::Loop => (time / self.duration).fract(),
627            LoopMode::PingPong => {
628                let t = time / self.duration;
629                let n = t.floor() as i32;
630                if n % 2 == 0 { t.fract() } else { 1.0 - t.fract() }
631            }
632            LoopMode::ClampForever => (time / self.duration).clamp(0.0, 1.0),
633        }
634    }
635}
636
637// ---------------------------------------------------------------------------
638// Retargeting editor
639// ---------------------------------------------------------------------------
640
641#[derive(Debug, Clone, Copy, PartialEq)]
642pub enum RetargetEditorTab {
643    SkeletonMapping,
644    PreviewComparison,
645    IkSettings,
646    ClipLibrary,
647}
648
649#[derive(Debug, Clone)]
650pub struct RetargetEditor {
651    pub retargeter: Option<AnimRetargeter>,
652    pub active_tab: RetargetEditorTab,
653    pub source_clips: Vec<AnimationClip>,
654    pub selected_clip: Option<usize>,
655    pub preview_time: f32,
656    pub preview_playing: bool,
657    pub show_bind_pose: bool,
658    pub show_ik_debug: bool,
659    pub selected_bone_src: Option<String>,
660    pub selected_bone_tgt: Option<String>,
661    pub filter_unmapped: bool,
662    pub search_query: String,
663}
664
665impl RetargetEditor {
666    pub fn new() -> Self {
667        let src = Skeleton::build_biped();
668        let tgt = Skeleton::build_biped();
669        let mut retargeter = AnimRetargeter::new(src, tgt);
670        retargeter.auto_map_by_name();
671        Self {
672            retargeter: Some(retargeter),
673            active_tab: RetargetEditorTab::SkeletonMapping,
674            source_clips: Vec::new(),
675            selected_clip: None,
676            preview_time: 0.0,
677            preview_playing: false,
678            show_bind_pose: false,
679            show_ik_debug: false,
680            selected_bone_src: None,
681            selected_bone_tgt: None,
682            filter_unmapped: false,
683            search_query: String::new(),
684        }
685    }
686
687    pub fn load_demo_clip(&mut self) {
688        let mut clip = AnimationClip::new("Walk_Cycle", 1.0, 30.0);
689        if let Some(ret) = &self.retargeter {
690            for bone in &ret.source_skeleton.bones {
691                let mut track = AnimationTrack::new(&bone.name);
692                // Generate simple walk animation
693                for f in 0..30 {
694                    let t = f as f32 / 30.0;
695                    let offset = Vec3::new(0.0, (t * std::f32::consts::TAU).sin() * 0.02, 0.0);
696                    track.add_keyframe(t, offset, Quat::IDENTITY, Vec3::ONE);
697                }
698                clip.tracks.push(track);
699            }
700        }
701        self.source_clips.push(clip);
702        self.selected_clip = Some(0);
703    }
704
705    pub fn update(&mut self, dt: f32) {
706        if self.preview_playing {
707            self.preview_time += dt;
708            if let Some(idx) = self.selected_clip {
709                let dur = self.source_clips.get(idx).map(|c| c.duration).unwrap_or(1.0);
710                self.preview_time = self.preview_time % dur;
711            }
712        }
713    }
714
715    pub fn mapping_count(&self) -> usize {
716        self.retargeter.as_ref().map(|r| r.mappings.len()).unwrap_or(0)
717    }
718}
719
720// ---------------------------------------------------------------------------
721// Tests
722// ---------------------------------------------------------------------------
723#[cfg(test)]
724mod tests {
725    use super::*;
726
727    #[test]
728    fn test_biped_skeleton() {
729        let skel = Skeleton::build_biped();
730        assert!(skel.bone_count() > 10);
731        assert!(skel.find_bone("Hips").is_some());
732        assert!(skel.find_bone("L_Hand").is_some());
733    }
734
735    #[test]
736    fn test_retargeter_auto_map() {
737        let src = Skeleton::build_biped();
738        let tgt = Skeleton::build_biped();
739        let mut ret = AnimRetargeter::new(src, tgt);
740        ret.auto_map_by_name();
741        assert!(ret.mappings.len() > 0);
742    }
743
744    #[test]
745    fn test_fabrik() {
746        let positions = vec![Vec3::ZERO, Vec3::new(0.0, 1.0, 0.0), Vec3::new(0.0, 2.0, 0.0)];
747        let lengths = vec![1.0, 1.0];
748        let mut chain = IkChain::new(vec![0, 1, 2], positions, lengths);
749        chain.target = Vec3::new(1.0, 1.0, 0.0);
750        chain.solve_fabrik(20, 0.001);
751        assert!(chain.positions[2].distance(chain.target) < 0.01);
752    }
753
754    #[test]
755    fn test_animation_track() {
756        let mut track = AnimationTrack::new("Hips");
757        track.add_keyframe(0.0, Vec3::ZERO, Quat::IDENTITY, Vec3::ONE);
758        track.add_keyframe(1.0, Vec3::Y, Quat::IDENTITY, Vec3::ONE);
759        let mat = track.sample(0.5);
760        let (_, _, t) = decompose_mat4(mat);
761        assert!((t.y - 0.5).abs() < 0.01);
762    }
763
764    #[test]
765    fn test_pose_lerp() {
766        let skel = Skeleton::build_biped();
767        let a = Pose::identity(skel.bone_count());
768        let b = Pose::identity(skel.bone_count());
769        let _ = a.lerp(&b, 0.5);
770    }
771}