Skip to main content

proof_engine/editor/
animation_compression.rs

1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6// ============================================================
7//  ANIMATION COMPRESSION
8//  Full implementation: keyframe reduction, quantization,
9//  curve fitting, delta compression, streaming, blend trees,
10//  retargeting, additive layers, LOD, and batch processing.
11// ============================================================
12
13// ─── Constants ───────────────────────────────────────────────────────────────
14
15const MAX_BONES: usize = 256;
16const MAX_KEYFRAMES: usize = 65536;
17const QUANTIZE_POS_BITS: u32 = 16;
18const QUANTIZE_ROT_BITS: u32 = 16;
19const QUANTIZE_SCALE_BITS: u32 = 8;
20const CHUNK_SIZE_FRAMES: usize = 64;
21const MAX_BLEND_TARGETS: usize = 16;
22const MAX_LOD_LEVELS: usize = 4;
23const SMALL3_SCALE: f32 = 0.7071068; // 1/sqrt(2)
24
25// ─── Basic Data Types ─────────────────────────────────────────────────────────
26
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct Transform {
29    pub position: Vec3,
30    pub rotation: Quat,
31    pub scale: Vec3,
32}
33
34impl Default for Transform {
35    fn default() -> Self {
36        Self {
37            position: Vec3::ZERO,
38            rotation: Quat::IDENTITY,
39            scale: Vec3::ONE,
40        }
41    }
42}
43
44impl Transform {
45    pub fn new(position: Vec3, rotation: Quat, scale: Vec3) -> Self {
46        Self { position, rotation, scale }
47    }
48
49    pub fn identity() -> Self {
50        Self::default()
51    }
52
53    pub fn lerp(&self, other: &Transform, t: f32) -> Transform {
54        Transform {
55            position: self.position.lerp(other.position, t),
56            rotation: self.rotation.slerp(other.rotation, t),
57            scale: self.scale.lerp(other.scale, t),
58        }
59    }
60
61    pub fn to_mat4(&self) -> Mat4 {
62        Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
63    }
64
65    pub fn inverse(&self) -> Transform {
66        let inv_rot = self.rotation.inverse();
67        let inv_scale = Vec3::new(1.0 / self.scale.x, 1.0 / self.scale.y, 1.0 / self.scale.z);
68        let inv_pos = inv_rot * (-self.position * inv_scale);
69        Transform {
70            position: inv_pos,
71            rotation: inv_rot,
72            scale: inv_scale,
73        }
74    }
75}
76
77#[derive(Debug, Clone, PartialEq)]
78pub struct Keyframe {
79    pub time: f32,
80    pub transform: Transform,
81}
82
83impl Keyframe {
84    pub fn new(time: f32, transform: Transform) -> Self {
85        Self { time, transform }
86    }
87}
88
89#[derive(Debug, Clone)]
90pub struct BoneTrack {
91    pub bone_index: u32,
92    pub keyframes: Vec<Keyframe>,
93    pub importance: f32, // 0..1, higher = more important, less aggressive reduction
94}
95
96impl BoneTrack {
97    pub fn new(bone_index: u32, importance: f32) -> Self {
98        Self {
99            bone_index,
100            keyframes: Vec::new(),
101            importance,
102        }
103    }
104
105    pub fn push(&mut self, kf: Keyframe) {
106        self.keyframes.push(kf);
107    }
108
109    pub fn duration(&self) -> f32 {
110        if self.keyframes.is_empty() {
111            return 0.0;
112        }
113        self.keyframes.last().unwrap().time - self.keyframes.first().unwrap().time
114    }
115}
116
117#[derive(Debug, Clone)]
118pub struct AnimationClip {
119    pub name: String,
120    pub frame_rate: f32,
121    pub duration: f32,
122    pub tracks: Vec<BoneTrack>,
123    pub looping: bool,
124}
125
126impl AnimationClip {
127    pub fn new(name: &str, frame_rate: f32, duration: f32) -> Self {
128        Self {
129            name: name.to_owned(),
130            frame_rate,
131            duration,
132            tracks: Vec::new(),
133            looping: false,
134        }
135    }
136
137    pub fn total_keyframes(&self) -> usize {
138        self.tracks.iter().map(|t| t.keyframes.len()).sum()
139    }
140
141    pub fn sample_at(&self, bone_index: u32, time: f32) -> Option<Transform> {
142        let track = self.tracks.iter().find(|t| t.bone_index == bone_index)?;
143        if track.keyframes.is_empty() {
144            return None;
145        }
146        if track.keyframes.len() == 1 {
147            return Some(track.keyframes[0].transform);
148        }
149        let t = time.clamp(
150            track.keyframes.first().unwrap().time,
151            track.keyframes.last().unwrap().time,
152        );
153        // Binary search for interval
154        let idx = track.keyframes.partition_point(|kf| kf.time <= t);
155        let idx = idx.min(track.keyframes.len() - 1);
156        if idx == 0 {
157            return Some(track.keyframes[0].transform);
158        }
159        let kf0 = &track.keyframes[idx - 1];
160        let kf1 = &track.keyframes[idx];
161        let dt = kf1.time - kf0.time;
162        let alpha = if dt > 1e-6 { (t - kf0.time) / dt } else { 0.0 };
163        Some(kf0.transform.lerp(&kf1.transform, alpha))
164    }
165}
166
167// ─── Error Metrics ───────────────────────────────────────────────────────────
168
169/// Geodesic distance on SO(3) between two quaternions (in radians).
170pub fn rotation_error_geodesic(a: Quat, b: Quat) -> f32 {
171    // dot product gives cos(angle/2), so angle = 2*acos(|dot|)
172    let d = a.dot(b).abs().min(1.0);
173    2.0 * d.acos()
174}
175
176/// L2 distance between two positions.
177pub fn position_error_l2(a: Vec3, b: Vec3) -> f32 {
178    (a - b).length()
179}
180
181/// L2 distance between two scale vectors.
182pub fn scale_error_l2(a: Vec3, b: Vec3) -> f32 {
183    (a - b).length()
184}
185
186/// Combined transform error weighted by component.
187pub fn transform_error(a: &Transform, b: &Transform, rot_weight: f32, pos_weight: f32, scale_weight: f32) -> f32 {
188    let re = rotation_error_geodesic(a.rotation, b.rotation) * rot_weight;
189    let pe = position_error_l2(a.position, b.position) * pos_weight;
190    let se = scale_error_l2(a.scale, b.scale) * scale_weight;
191    re + pe + se
192}
193
194// ─── Ramer-Douglas-Peucker Curve Simplification ──────────────────────────────
195
196/// RDP simplification for position curves.
197/// Returns indices of keyframes to keep.
198pub fn rdp_simplify_positions(keyframes: &[Keyframe], epsilon: f32) -> Vec<usize> {
199    if keyframes.len() <= 2 {
200        return (0..keyframes.len()).collect();
201    }
202    let mut result = Vec::new();
203    rdp_recursive_positions(keyframes, 0, keyframes.len() - 1, epsilon, &mut result);
204    result.sort_unstable();
205    result.dedup();
206    result
207}
208
209fn rdp_recursive_positions(
210    keyframes: &[Keyframe],
211    start: usize,
212    end: usize,
213    epsilon: f32,
214    result: &mut Vec<usize>,
215) {
216    if start >= end {
217        result.push(start);
218        return;
219    }
220    result.push(start);
221    result.push(end);
222    if end - start < 2 {
223        return;
224    }
225
226    let p_start = keyframes[start].transform.position;
227    let p_end = keyframes[end].transform.position;
228    let t_start = keyframes[start].time;
229    let t_end = keyframes[end].time;
230    let dt = t_end - t_start;
231
232    let mut max_dist = 0.0f32;
233    let mut max_idx = start + 1;
234
235    for i in (start + 1)..end {
236        let t = keyframes[i].time;
237        let alpha = if dt > 1e-9 { (t - t_start) / dt } else { 0.0 };
238        let interpolated = p_start.lerp(p_end, alpha);
239        let d = position_error_l2(keyframes[i].transform.position, interpolated);
240        if d > max_dist {
241            max_dist = d;
242            max_idx = i;
243        }
244    }
245
246    if max_dist > epsilon {
247        rdp_recursive_positions(keyframes, start, max_idx, epsilon, result);
248        rdp_recursive_positions(keyframes, max_idx, end, epsilon, result);
249    }
250}
251
252/// RDP simplification for rotation curves using geodesic error.
253pub fn rdp_simplify_rotations(keyframes: &[Keyframe], epsilon: f32) -> Vec<usize> {
254    if keyframes.len() <= 2 {
255        return (0..keyframes.len()).collect();
256    }
257    let mut result = Vec::new();
258    rdp_recursive_rotations(keyframes, 0, keyframes.len() - 1, epsilon, &mut result);
259    result.sort_unstable();
260    result.dedup();
261    result
262}
263
264fn rdp_recursive_rotations(
265    keyframes: &[Keyframe],
266    start: usize,
267    end: usize,
268    epsilon: f32,
269    result: &mut Vec<usize>,
270) {
271    result.push(start);
272    result.push(end);
273    if end - start < 2 {
274        return;
275    }
276
277    let q_start = keyframes[start].transform.rotation;
278    let q_end = keyframes[end].transform.rotation;
279    let t_start = keyframes[start].time;
280    let t_end = keyframes[end].time;
281    let dt = t_end - t_start;
282
283    let mut max_err = 0.0f32;
284    let mut max_idx = start + 1;
285
286    for i in (start + 1)..end {
287        let alpha = if dt > 1e-9 { (keyframes[i].time - t_start) / dt } else { 0.0 };
288        let interpolated = q_start.slerp(q_end, alpha);
289        let e = rotation_error_geodesic(keyframes[i].transform.rotation, interpolated);
290        if e > max_err {
291            max_err = e;
292            max_idx = i;
293        }
294    }
295
296    if max_err > epsilon {
297        rdp_recursive_rotations(keyframes, start, max_idx, epsilon, result);
298        rdp_recursive_rotations(keyframes, max_idx, end, epsilon, result);
299    }
300}
301
302/// Combined RDP reduction on a BoneTrack with adaptive tolerance.
303pub fn rdp_reduce_track(track: &BoneTrack, base_pos_eps: f32, base_rot_eps: f32) -> BoneTrack {
304    if track.keyframes.is_empty() {
305        return track.clone();
306    }
307    // Adaptive tolerance: less important bones get larger epsilon (more reduction)
308    let importance = track.importance.clamp(0.0, 1.0);
309    let pos_eps = base_pos_eps / (0.1 + 0.9 * importance);
310    let rot_eps = base_rot_eps / (0.1 + 0.9 * importance);
311
312    // Reduce positions
313    let pos_keep = rdp_simplify_positions(&track.keyframes, pos_eps);
314    // Reduce rotations
315    let rot_keep = rdp_simplify_rotations(&track.keyframes, rot_eps);
316
317    // Union of kept indices
318    let mut keep_set: HashSet<usize> = HashSet::new();
319    for &i in &pos_keep { keep_set.insert(i); }
320    for &i in &rot_keep { keep_set.insert(i); }
321    // Always keep first and last
322    keep_set.insert(0);
323    keep_set.insert(track.keyframes.len() - 1);
324
325    let mut indices: Vec<usize> = keep_set.into_iter().collect();
326    indices.sort_unstable();
327
328    let new_keyframes: Vec<Keyframe> = indices.iter().map(|&i| track.keyframes[i].clone()).collect();
329    BoneTrack {
330        bone_index: track.bone_index,
331        keyframes: new_keyframes,
332        importance: track.importance,
333    }
334}
335
336// ─── Quantization: Position (16-bit fixed point) ─────────────────────────────
337
338#[derive(Debug, Clone)]
339pub struct PositionBounds {
340    pub min: Vec3,
341    pub max: Vec3,
342}
343
344impl PositionBounds {
345    pub fn from_track(track: &BoneTrack) -> Self {
346        if track.keyframes.is_empty() {
347            return Self { min: Vec3::ZERO, max: Vec3::ONE };
348        }
349        let mut min = Vec3::splat(f32::MAX);
350        let mut max = Vec3::splat(f32::MIN);
351        for kf in &track.keyframes {
352            min = min.min(kf.transform.position);
353            max = max.max(kf.transform.position);
354        }
355        // Expand slightly to avoid boundary rounding issues
356        let expand = (max - min) * 0.001 + Vec3::splat(1e-6);
357        Self { min: min - expand, max: max + expand }
358    }
359
360    pub fn range(&self) -> Vec3 {
361        self.max - self.min
362    }
363}
364
365/// Quantize a position to 16-bit integers per component.
366pub fn quantize_position_16(pos: Vec3, bounds: &PositionBounds) -> [u16; 3] {
367    let range = bounds.range();
368    let norm = (pos - bounds.min) / range;
369    let qx = (norm.x.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16;
370    let qy = (norm.y.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16;
371    let qz = (norm.z.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16;
372    [qx, qy, qz]
373}
374
375/// Dequantize a 16-bit position.
376pub fn dequantize_position_16(q: [u16; 3], bounds: &PositionBounds) -> Vec3 {
377    let range = bounds.range();
378    let nx = q[0] as f32 / 65535.0;
379    let ny = q[1] as f32 / 65535.0;
380    let nz = q[2] as f32 / 65535.0;
381    bounds.min + Vec3::new(nx, ny, nz) * range
382}
383
384// ─── Quantization: Rotation (Smallest-3 method) ──────────────────────────────
385
386/// Represents a compressed quaternion using the smallest-3 method.
387/// Stores index of the largest component (2 bits) and the other 3 as i16.
388#[derive(Debug, Clone, Copy)]
389pub struct CompressedQuat {
390    pub largest_component: u8, // 0,1,2,3 = w,x,y,z
391    pub components: [i16; 3],  // smallest 3 components, scaled by 32767*sqrt(2)
392}
393
394pub fn compress_quat_smallest3(q: Quat) -> CompressedQuat {
395    // Ensure positive w hemisphere
396    let q = if q.w < 0.0 { Quat::from_xyzw(-q.x, -q.y, -q.z, -q.w) } else { q };
397
398    let components = [q.w, q.x, q.y, q.z];
399    let abs = [q.w.abs(), q.x.abs(), q.y.abs(), q.z.abs()];
400
401    // Find largest component index
402    let mut largest = 0usize;
403    let mut largest_val = abs[0];
404    for i in 1..4 {
405        if abs[i] > largest_val {
406            largest_val = abs[i];
407            largest = i;
408        }
409    }
410
411    // Collect the other 3 components
412    let mut small = [0.0f32; 3];
413    let mut si = 0;
414    for i in 0..4 {
415        if i != largest {
416            small[si] = components[i];
417            si += 1;
418        }
419    }
420
421    // Encode: range of each small component is [-1/sqrt(2), 1/sqrt(2)]
422    // Map to [-32767, 32767]
423    let scale = 32767.0 / SMALL3_SCALE;
424    CompressedQuat {
425        largest_component: largest as u8,
426        components: [
427            (small[0] * scale).round().clamp(-32767.0, 32767.0) as i16,
428            (small[1] * scale).round().clamp(-32767.0, 32767.0) as i16,
429            (small[2] * scale).round().clamp(-32767.0, 32767.0) as i16,
430        ],
431    }
432}
433
434pub fn decompress_quat_smallest3(cq: &CompressedQuat) -> Quat {
435    let inv_scale = SMALL3_SCALE / 32767.0;
436    let s0 = cq.components[0] as f32 * inv_scale;
437    let s1 = cq.components[1] as f32 * inv_scale;
438    let s2 = cq.components[2] as f32 * inv_scale;
439
440    // Reconstruct largest from unit constraint
441    let sum_sq = s0 * s0 + s1 * s1 + s2 * s2;
442    let largest = (1.0 - sum_sq).max(0.0).sqrt();
443
444    let (w, x, y, z) = match cq.largest_component {
445        0 => (largest, s0, s1, s2),
446        1 => (s0, largest, s1, s2),
447        2 => (s0, s1, largest, s2),
448        _ => (s0, s1, s2, largest),
449    };
450
451    Quat::from_xyzw(x, y, z, w).normalize()
452}
453
454// ─── Quantization: Scale (8-bit log encoding) ────────────────────────────────
455
456/// Encode scale component to 8-bit log encoding.
457/// Assumes scale is in [1/16, 16] range.
458pub fn quantize_scale_log8(s: f32) -> u8 {
459    // log2 range: [-4, 4], map to [0, 255]
460    let log_s = s.abs().max(1e-6).log2();
461    let norm = (log_s + 4.0) / 8.0; // [-4,4] -> [0,1]
462    (norm.clamp(0.0, 1.0) * 255.0 + 0.5) as u8
463}
464
465pub fn dequantize_scale_log8(q: u8) -> f32 {
466    let norm = q as f32 / 255.0;
467    let log_s = norm * 8.0 - 4.0;
468    2.0f32.powf(log_s)
469}
470
471pub fn quantize_scale_vec_log8(s: Vec3) -> [u8; 3] {
472    [
473        quantize_scale_log8(s.x),
474        quantize_scale_log8(s.y),
475        quantize_scale_log8(s.z),
476    ]
477}
478
479pub fn dequantize_scale_vec_log8(q: [u8; 3]) -> Vec3 {
480    Vec3::new(
481        dequantize_scale_log8(q[0]),
482        dequantize_scale_log8(q[1]),
483        dequantize_scale_log8(q[2]),
484    )
485}
486
487// ─── Compressed Keyframe Storage ─────────────────────────────────────────────
488
489#[derive(Debug, Clone)]
490pub struct CompressedKeyframe {
491    pub time_ticks: u32,   // time in ticks (frame number * 1000)
492    pub position: [u16; 3],
493    pub rotation: CompressedQuat,
494    pub scale: [u8; 3],
495}
496
497#[derive(Debug, Clone)]
498pub struct CompressedTrack {
499    pub bone_index: u32,
500    pub pos_bounds: PositionBounds,
501    pub tick_rate: f32,
502    pub keyframes: Vec<CompressedKeyframe>,
503}
504
505impl CompressedTrack {
506    pub fn from_track(track: &BoneTrack, tick_rate: f32) -> Self {
507        let bounds = PositionBounds::from_track(track);
508        let keyframes = track.keyframes.iter().map(|kf| {
509            let ticks = (kf.time * tick_rate * 1000.0) as u32;
510            CompressedKeyframe {
511                time_ticks: ticks,
512                position: quantize_position_16(kf.transform.position, &bounds),
513                rotation: compress_quat_smallest3(kf.transform.rotation),
514                scale: quantize_scale_vec_log8(kf.transform.scale),
515            }
516        }).collect();
517        CompressedTrack {
518            bone_index: track.bone_index,
519            pos_bounds: bounds,
520            tick_rate,
521            keyframes,
522        }
523    }
524
525    pub fn to_track(&self) -> BoneTrack {
526        let keyframes = self.keyframes.iter().map(|ckf| {
527            let time = ckf.time_ticks as f32 / (self.tick_rate * 1000.0);
528            let position = dequantize_position_16(ckf.position, &self.pos_bounds);
529            let rotation = decompress_quat_smallest3(&ckf.rotation);
530            let scale = dequantize_scale_vec_log8(ckf.scale);
531            Keyframe { time, transform: Transform { position, rotation, scale } }
532        }).collect();
533        BoneTrack {
534            bone_index: self.bone_index,
535            keyframes,
536            importance: 1.0,
537        }
538    }
539
540    pub fn byte_size(&self) -> usize {
541        // bone_index(4) + bounds(24) + tick_rate(4) + per_keyframe
542        // Each keyframe: time(4) + pos(6) + rot(8) + scale(3) = 21 bytes
543        4 + 24 + 4 + self.keyframes.len() * 21
544    }
545}
546
547// ─── Cubic Hermite Spline Fitting ────────────────────────────────────────────
548
549/// A cubic Hermite spline segment.
550#[derive(Debug, Clone, Copy)]
551pub struct HermiteSegment {
552    pub t0: f32,
553    pub t1: f32,
554    pub p0: Vec3,
555    pub p1: Vec3,
556    pub m0: Vec3, // incoming tangent at p0
557    pub m1: Vec3, // outgoing tangent at p1
558}
559
560impl HermiteSegment {
561    pub fn evaluate(&self, t: f32) -> Vec3 {
562        let dt = self.t1 - self.t0;
563        let s = if dt > 1e-9 { (t - self.t0) / dt } else { 0.0 };
564        let s2 = s * s;
565        let s3 = s2 * s;
566        // Cubic Hermite basis functions
567        let h00 = 2.0 * s3 - 3.0 * s2 + 1.0;
568        let h10 = s3 - 2.0 * s2 + s;
569        let h01 = -2.0 * s3 + 3.0 * s2;
570        let h11 = s3 - s2;
571        self.p0 * h00 + self.m0 * (h10 * dt) + self.p1 * h01 + self.m1 * (h11 * dt)
572    }
573
574    pub fn max_error_vs_keyframes(&self, keyframes: &[Keyframe]) -> f32 {
575        let mut max_err = 0.0f32;
576        for kf in keyframes {
577            if kf.time >= self.t0 && kf.time <= self.t1 {
578                let approx = self.evaluate(kf.time);
579                let err = position_error_l2(approx, kf.transform.position);
580                if err > max_err { max_err = err; }
581            }
582        }
583        max_err
584    }
585}
586
587/// Estimate tangents for a sequence of keyframes using Catmull-Rom style.
588pub fn estimate_tangents_catmull_rom(keyframes: &[Keyframe]) -> Vec<Vec3> {
589    let n = keyframes.len();
590    let mut tangents = vec![Vec3::ZERO; n];
591    for i in 0..n {
592        if i == 0 {
593            if n > 1 {
594                let dt = keyframes[1].time - keyframes[0].time;
595                if dt > 1e-9 {
596                    tangents[0] = (keyframes[1].transform.position - keyframes[0].transform.position) / dt;
597                }
598            }
599        } else if i == n - 1 {
600            let dt = keyframes[n-1].time - keyframes[n-2].time;
601            if dt > 1e-9 {
602                tangents[n-1] = (keyframes[n-1].transform.position - keyframes[n-2].transform.position) / dt;
603            }
604        } else {
605            let dt_prev = keyframes[i].time - keyframes[i-1].time;
606            let dt_next = keyframes[i+1].time - keyframes[i].time;
607            let dt_total = dt_prev + dt_next;
608            if dt_total > 1e-9 {
609                tangents[i] = (keyframes[i+1].transform.position - keyframes[i-1].transform.position) / dt_total;
610            }
611        }
612    }
613    tangents
614}
615
616/// Build Hermite spline segments from keyframe sequence.
617pub fn build_hermite_spline(keyframes: &[Keyframe]) -> Vec<HermiteSegment> {
618    if keyframes.len() < 2 {
619        return Vec::new();
620    }
621    let tangents = estimate_tangents_catmull_rom(keyframes);
622    let mut segments = Vec::new();
623    for i in 0..(keyframes.len() - 1) {
624        segments.push(HermiteSegment {
625            t0: keyframes[i].time,
626            t1: keyframes[i+1].time,
627            p0: keyframes[i].transform.position,
628            p1: keyframes[i+1].transform.position,
629            m0: tangents[i],
630            m1: tangents[i+1],
631        });
632    }
633    segments
634}
635
636/// Least-squares tangent estimation for more accurate fitting.
637/// Uses a 3-point stencil with proper weighting.
638pub fn estimate_tangents_least_squares(keyframes: &[Keyframe]) -> Vec<Vec3> {
639    let n = keyframes.len();
640    let mut tangents = vec![Vec3::ZERO; n];
641    for i in 0..n {
642        if i == 0 || i == n - 1 {
643            // Endpoint: one-sided
644            if i == 0 && n > 1 {
645                let dt = keyframes[1].time - keyframes[0].time;
646                if dt > 1e-9 {
647                    tangents[0] = (keyframes[1].transform.position - keyframes[0].transform.position) / dt;
648                }
649            } else if i == n - 1 && n > 1 {
650                let dt = keyframes[n-1].time - keyframes[n-2].time;
651                if dt > 1e-9 {
652                    tangents[n-1] = (keyframes[n-1].transform.position - keyframes[n-2].transform.position) / dt;
653                }
654            }
655        } else {
656            // Interior: weighted least squares using neighbors
657            let dt_m = keyframes[i].time - keyframes[i-1].time;
658            let dt_p = keyframes[i+1].time - keyframes[i].time;
659            let w_m = 1.0 / (dt_m * dt_m + 1e-9);
660            let w_p = 1.0 / (dt_p * dt_p + 1e-9);
661            let sum_w = w_m + w_p;
662            if sum_w > 1e-9 {
663                let slope_m = if dt_m > 1e-9 {
664                    (keyframes[i].transform.position - keyframes[i-1].transform.position) / dt_m
665                } else {
666                    Vec3::ZERO
667                };
668                let slope_p = if dt_p > 1e-9 {
669                    (keyframes[i+1].transform.position - keyframes[i].transform.position) / dt_p
670                } else {
671                    Vec3::ZERO
672                };
673                tangents[i] = (slope_m * w_m + slope_p * w_p) / sum_w;
674            }
675        }
676    }
677    tangents
678}
679
680/// Error-bounded curve reduction using Hermite spline fitting.
681pub fn hermite_reduce_track(track: &BoneTrack, max_error: f32) -> BoneTrack {
682    if track.keyframes.len() <= 2 {
683        return track.clone();
684    }
685    let keyframes = &track.keyframes;
686    let mut keep = vec![false; keyframes.len()];
687    keep[0] = true;
688    keep[keyframes.len() - 1] = true;
689
690    // Iteratively add keyframes where error exceeds threshold
691    let mut changed = true;
692    while changed {
693        changed = false;
694        // Build spline from currently kept keyframes
695        let kept: Vec<Keyframe> = keyframes.iter().enumerate()
696            .filter(|(i, _)| keep[*i])
697            .map(|(_, kf)| kf.clone())
698            .collect();
699        let tangents = estimate_tangents_least_squares(&kept);
700        // For each gap, check max error
701        for seg_i in 0..(kept.len().saturating_sub(1)) {
702            let seg = HermiteSegment {
703                t0: kept[seg_i].time,
704                t1: kept[seg_i + 1].time,
705                p0: kept[seg_i].transform.position,
706                p1: kept[seg_i + 1].transform.position,
707                m0: tangents[seg_i],
708                m1: tangents[seg_i + 1],
709            };
710            // Find original keyframes in this range
711            let in_range: Vec<Keyframe> = keyframes.iter()
712                .filter(|kf| kf.time > seg.t0 && kf.time < seg.t1)
713                .cloned()
714                .collect();
715            if in_range.is_empty() { continue; }
716            let err = seg.max_error_vs_keyframes(&in_range);
717            if err > max_error {
718                // Find the worst-error keyframe and add it
719                let mut worst_err = 0.0f32;
720                let mut worst_time = seg.t0;
721                for kf in &in_range {
722                    let approx = seg.evaluate(kf.time);
723                    let e = position_error_l2(approx, kf.transform.position);
724                    if e > worst_err {
725                        worst_err = e;
726                        worst_time = kf.time;
727                    }
728                }
729                // Mark this keyframe as kept
730                if let Some(idx) = keyframes.iter().position(|kf| kf.time == worst_time) {
731                    if !keep[idx] {
732                        keep[idx] = true;
733                        changed = true;
734                    }
735                }
736            }
737        }
738    }
739
740    let new_keyframes: Vec<Keyframe> = keyframes.iter().enumerate()
741        .filter(|(i, _)| keep[*i])
742        .map(|(_, kf)| kf.clone())
743        .collect();
744    BoneTrack {
745        bone_index: track.bone_index,
746        keyframes: new_keyframes,
747        importance: track.importance,
748    }
749}
750
751// ─── Delta Compression ────────────────────────────────────────────────────────
752
753#[derive(Debug, Clone)]
754pub struct ReferencePose {
755    pub transforms: Vec<Transform>, // indexed by bone
756}
757
758impl ReferencePose {
759    pub fn new(n_bones: usize) -> Self {
760        Self { transforms: vec![Transform::identity(); n_bones] }
761    }
762
763    pub fn from_clip_frame0(clip: &AnimationClip, n_bones: usize) -> Self {
764        let mut transforms = vec![Transform::identity(); n_bones];
765        for track in &clip.tracks {
766            let idx = track.bone_index as usize;
767            if idx < n_bones {
768                if let Some(t) = track.keyframes.first() {
769                    transforms[idx] = t.transform;
770                }
771            }
772        }
773        Self { transforms }
774    }
775}
776
777/// Delta of a transform from reference.
778#[derive(Debug, Clone, Copy)]
779pub struct TransformDelta {
780    pub pos_delta: Vec3,
781    pub rot_delta: Quat,
782    pub scale_delta: Vec3,
783}
784
785impl TransformDelta {
786    pub fn compute(reference: &Transform, current: &Transform) -> Self {
787        let pos_delta = current.position - reference.position;
788        let rot_delta = reference.rotation.inverse() * current.rotation;
789        let scale_delta = current.scale - reference.scale;
790        Self { pos_delta, rot_delta, scale_delta }
791    }
792
793    pub fn apply(&self, reference: &Transform) -> Transform {
794        Transform {
795            position: reference.position + self.pos_delta,
796            rotation: reference.rotation * self.rot_delta,
797            scale: reference.scale + self.scale_delta,
798        }
799    }
800
801    pub fn is_near_zero(&self, pos_thresh: f32, rot_thresh: f32, scale_thresh: f32) -> bool {
802        self.pos_delta.length() < pos_thresh
803            && rotation_error_geodesic(self.rot_delta, Quat::IDENTITY) < rot_thresh
804            && self.scale_delta.length() < scale_thresh
805    }
806}
807
808/// Variable-length encoded delta: near-zero = 1 byte, otherwise full.
809#[derive(Debug, Clone)]
810pub enum EncodedDelta {
811    Zero,                   // 0 bytes (implicit)
812    SmallPos([i8; 3]),      // 3 bytes, centimeter-precision
813    FullPos(Vec3),          // 12 bytes
814    SmallRot([i8; 4]),      // 4 bytes, rough rotation
815    FullRot(Quat),          // 16 bytes
816    SmallScale([i8; 3]),    // 3 bytes
817    FullScale(Vec3),        // 12 bytes
818}
819
820/// Encode a position delta with variable-length encoding.
821pub fn encode_pos_delta(delta: Vec3, threshold: f32) -> EncodedDelta {
822    let len = delta.length();
823    if len < 1e-6 {
824        return EncodedDelta::Zero;
825    }
826    // Try to fit in i8 range (0.5cm precision)
827    let cx = (delta.x * 200.0).round();
828    let cy = (delta.y * 200.0).round();
829    let cz = (delta.z * 200.0).round();
830    if cx.abs() <= 127.0 && cy.abs() <= 127.0 && cz.abs() <= 127.0 {
831        EncodedDelta::SmallPos([cx as i8, cy as i8, cz as i8])
832    } else {
833        EncodedDelta::FullPos(delta)
834    }
835}
836
837pub fn decode_pos_delta(enc: &EncodedDelta) -> Vec3 {
838    match enc {
839        EncodedDelta::Zero => Vec3::ZERO,
840        EncodedDelta::SmallPos(b) => Vec3::new(b[0] as f32 / 200.0, b[1] as f32 / 200.0, b[2] as f32 / 200.0),
841        EncodedDelta::FullPos(v) => *v,
842        _ => Vec3::ZERO,
843    }
844}
845
846#[derive(Debug, Clone)]
847pub struct DeltaFrame {
848    pub bone_index: u32,
849    pub time: f32,
850    pub delta: TransformDelta,
851    pub is_keyframe: bool, // if false, can be dropped
852}
853
854#[derive(Debug, Clone)]
855pub struct DeltaCompressedTrack {
856    pub bone_index: u32,
857    pub reference: Transform,
858    pub frames: Vec<DeltaFrame>,
859}
860
861impl DeltaCompressedTrack {
862    pub fn from_track(track: &BoneTrack, reference: &Transform) -> Self {
863        let frames = track.keyframes.iter().enumerate().map(|(i, kf)| {
864            let delta = TransformDelta::compute(reference, &kf.transform);
865            DeltaFrame {
866                bone_index: track.bone_index,
867                time: kf.time,
868                delta,
869                is_keyframe: i == 0 || i == track.keyframes.len() - 1,
870            }
871        }).collect();
872        DeltaCompressedTrack {
873            bone_index: track.bone_index,
874            reference: *reference,
875            frames,
876        }
877    }
878
879    pub fn to_track(&self) -> BoneTrack {
880        let keyframes = self.frames.iter().map(|df| {
881            Keyframe {
882                time: df.time,
883                transform: df.delta.apply(&self.reference),
884            }
885        }).collect();
886        BoneTrack {
887            bone_index: self.bone_index,
888            keyframes,
889            importance: 1.0,
890        }
891    }
892
893    /// Drop near-zero delta frames (except keyframes).
894    pub fn cull_zero_deltas(&mut self, pos_thresh: f32, rot_thresh: f32, scale_thresh: f32) {
895        self.frames.retain(|df| {
896            df.is_keyframe || !df.delta.is_near_zero(pos_thresh, rot_thresh, scale_thresh)
897        });
898    }
899}
900
901// ─── Animation Streaming ─────────────────────────────────────────────────────
902
903#[derive(Debug, Clone)]
904pub struct AnimationChunk {
905    pub chunk_id: u32,
906    pub start_frame: u32,
907    pub end_frame: u32,
908    pub tracks: Vec<CompressedTrack>,
909    pub byte_size: usize,
910}
911
912impl AnimationChunk {
913    pub fn new(chunk_id: u32, start_frame: u32, end_frame: u32) -> Self {
914        Self {
915            chunk_id,
916            start_frame,
917            end_frame,
918            tracks: Vec::new(),
919            byte_size: 0,
920        }
921    }
922
923    pub fn add_track(&mut self, track: CompressedTrack) {
924        self.byte_size += track.byte_size();
925        self.tracks.push(track);
926    }
927}
928
929#[derive(Debug, Clone)]
930pub struct StreamingAnimationAsset {
931    pub name: String,
932    pub total_frames: u32,
933    pub frame_rate: f32,
934    pub chunks: Vec<AnimationChunk>,
935    pub chunk_size: usize,
936    pub loaded_chunks: HashSet<u32>,
937}
938
939impl StreamingAnimationAsset {
940    pub fn new(name: &str, total_frames: u32, frame_rate: f32, chunk_size: usize) -> Self {
941        Self {
942            name: name.to_owned(),
943            total_frames,
944            frame_rate,
945            chunks: Vec::new(),
946            chunk_size,
947            loaded_chunks: HashSet::new(),
948        }
949    }
950
951    pub fn chunk_for_frame(&self, frame: u32) -> Option<u32> {
952        if self.chunk_size == 0 { return None; }
953        let chunk_id = frame / self.chunk_size as u32;
954        Some(chunk_id)
955    }
956
957    pub fn is_chunk_loaded(&self, chunk_id: u32) -> bool {
958        self.loaded_chunks.contains(&chunk_id)
959    }
960
961    pub fn mark_loaded(&mut self, chunk_id: u32) {
962        self.loaded_chunks.insert(chunk_id);
963    }
964
965    pub fn unload_chunk(&mut self, chunk_id: u32) {
966        self.loaded_chunks.remove(&chunk_id);
967        self.chunks.retain(|c| c.chunk_id != chunk_id);
968    }
969
970    /// Predict which chunks to prefetch based on current playback position and velocity.
971    pub fn prefetch_prediction(
972        &self,
973        current_frame: u32,
974        playback_speed: f32,
975        lookahead_seconds: f32,
976    ) -> Vec<u32> {
977        let lookahead_frames = (playback_speed.abs() * lookahead_seconds * self.frame_rate) as u32;
978        let end_frame = (current_frame + lookahead_frames).min(self.total_frames.saturating_sub(1));
979        let start_chunk = self.chunk_for_frame(current_frame).unwrap_or(0);
980        let end_chunk = self.chunk_for_frame(end_frame).unwrap_or(0);
981        let mut needed = Vec::new();
982        for c in start_chunk..=end_chunk {
983            if !self.is_chunk_loaded(c) {
984                needed.push(c);
985            }
986        }
987        needed
988    }
989
990    pub fn build_from_clip(clip: &AnimationClip, chunk_size: usize) -> Self {
991        let total_frames = (clip.duration * clip.frame_rate) as u32;
992        let mut asset = Self::new(&clip.name, total_frames, clip.frame_rate, chunk_size);
993        let n_chunks = (total_frames as usize + chunk_size - 1) / chunk_size;
994        for ci in 0..n_chunks {
995            let start = (ci * chunk_size) as u32;
996            let end = (((ci + 1) * chunk_size) as u32 - 1).min(total_frames - 1);
997            let mut chunk = AnimationChunk::new(ci as u32, start, end);
998            for track in &clip.tracks {
999                // Filter keyframes to this chunk's time range
1000                let t_start = start as f32 / clip.frame_rate;
1001                let t_end = end as f32 / clip.frame_rate;
1002                let kfs: Vec<Keyframe> = track.keyframes.iter()
1003                    .filter(|kf| kf.time >= t_start - 0.001 && kf.time <= t_end + 0.001)
1004                    .cloned()
1005                    .collect();
1006                if !kfs.is_empty() {
1007                    let sub_track = BoneTrack { bone_index: track.bone_index, keyframes: kfs, importance: track.importance };
1008                    chunk.add_track(CompressedTrack::from_track(&sub_track, clip.frame_rate));
1009                }
1010            }
1011            asset.chunks.push(chunk);
1012        }
1013        asset
1014    }
1015}
1016
1017// ─── Blend Tree Compression ───────────────────────────────────────────────────
1018
1019#[derive(Debug, Clone)]
1020pub struct BlendNode {
1021    pub name: String,
1022    pub clip: Option<AnimationClip>,
1023    pub children: Vec<BlendNode>,
1024    pub blend_weights: Vec<f32>,
1025}
1026
1027impl BlendNode {
1028    pub fn leaf(name: &str, clip: AnimationClip) -> Self {
1029        Self {
1030            name: name.to_owned(),
1031            clip: Some(clip),
1032            children: Vec::new(),
1033            blend_weights: Vec::new(),
1034        }
1035    }
1036
1037    pub fn blend(name: &str, children: Vec<BlendNode>, weights: Vec<f32>) -> Self {
1038        Self {
1039            name: name.to_owned(),
1040            clip: None,
1041            children,
1042            blend_weights: weights,
1043        }
1044    }
1045}
1046
1047/// Extract a shared base pose from multiple clips for diff encoding.
1048pub fn extract_shared_base_pose(clips: &[&AnimationClip], n_bones: usize) -> ReferencePose {
1049    let mut avg_transforms = vec![Vec::new(); n_bones];
1050    for clip in clips {
1051        for track in &clip.tracks {
1052            let idx = track.bone_index as usize;
1053            if idx < n_bones {
1054                if let Some(kf) = track.keyframes.first() {
1055                    avg_transforms[idx].push(kf.transform);
1056                }
1057            }
1058        }
1059    }
1060    let transforms = avg_transforms.iter().map(|ts| {
1061        if ts.is_empty() {
1062            Transform::identity()
1063        } else {
1064            // Average position and scale; SLERP all rotations
1065            let n = ts.len() as f32;
1066            let avg_pos = ts.iter().fold(Vec3::ZERO, |a, t| a + t.position) / n;
1067            let avg_scale = ts.iter().fold(Vec3::ZERO, |a, t| a + t.scale) / n;
1068            // Average quaternion: use iterative normalization
1069            let mut avg_rot = ts[0].rotation;
1070            for t in ts.iter().skip(1) {
1071                avg_rot = avg_rot.slerp(t.rotation, 1.0 / n);
1072            }
1073            Transform { position: avg_pos, rotation: avg_rot.normalize(), scale: avg_scale }
1074        }
1075    }).collect();
1076    ReferencePose { transforms }
1077}
1078
1079#[derive(Debug, Clone)]
1080pub struct BlendTreeCompressed {
1081    pub base_pose: ReferencePose,
1082    pub clip_deltas: Vec<(String, Vec<DeltaCompressedTrack>)>,
1083}
1084
1085impl BlendTreeCompressed {
1086    pub fn compress(clips: &[&AnimationClip], n_bones: usize) -> Self {
1087        let base_pose = extract_shared_base_pose(clips, n_bones);
1088        let clip_deltas = clips.iter().map(|clip| {
1089            let tracks = clip.tracks.iter().map(|track| {
1090                let bone_idx = track.bone_index as usize;
1091                let reference = if bone_idx < base_pose.transforms.len() {
1092                    base_pose.transforms[bone_idx]
1093                } else {
1094                    Transform::identity()
1095                };
1096                DeltaCompressedTrack::from_track(track, &reference)
1097            }).collect();
1098            (clip.name.clone(), tracks)
1099        }).collect();
1100        Self { base_pose, clip_deltas }
1101    }
1102
1103    pub fn decompress_clip(&self, name: &str) -> Option<Vec<BoneTrack>> {
1104        let (_, delta_tracks) = self.clip_deltas.iter().find(|(n, _)| n == name)?;
1105        Some(delta_tracks.iter().map(|dt| dt.to_track()).collect())
1106    }
1107}
1108
1109// ─── Retargeting Compression ──────────────────────────────────────────────────
1110
1111#[derive(Debug, Clone)]
1112pub struct BoneMapping {
1113    pub source_bone: u32,
1114    pub target_bone: u32,
1115    pub scale_factor: f32,
1116    pub rotation_offset: Quat,
1117    pub position_offset: Vec3,
1118}
1119
1120impl BoneMapping {
1121    pub fn new(source: u32, target: u32) -> Self {
1122        Self {
1123            source_bone: source,
1124            target_bone: target,
1125            scale_factor: 1.0,
1126            rotation_offset: Quat::IDENTITY,
1127            position_offset: Vec3::ZERO,
1128        }
1129    }
1130
1131    pub fn with_scale(mut self, s: f32) -> Self {
1132        self.scale_factor = s;
1133        self
1134    }
1135
1136    pub fn with_rotation_offset(mut self, q: Quat) -> Self {
1137        self.rotation_offset = q;
1138        self
1139    }
1140}
1141
1142/// Decompose a quaternion into swing and twist components around a given axis.
1143pub fn swing_twist_decompose(q: Quat, twist_axis: Vec3) -> (Quat, Quat) {
1144    // Project rotation onto the twist axis
1145    let q_vec = Vec3::new(q.x, q.y, q.z);
1146    let proj = q_vec.dot(twist_axis) * twist_axis;
1147    let mut twist = Quat::from_xyzw(proj.x, proj.y, proj.z, q.w);
1148    if twist.length_squared() < 1e-10 {
1149        twist = Quat::IDENTITY;
1150    } else {
1151        twist = twist.normalize();
1152    }
1153    let swing = q * twist.inverse();
1154    (swing, twist)
1155}
1156
1157/// Retarget a single transform using bone mapping.
1158pub fn retarget_transform(t: &Transform, mapping: &BoneMapping) -> Transform {
1159    let (swing, twist) = swing_twist_decompose(t.rotation, Vec3::Y);
1160    // Apply retargeting: scale position, adjust rotation
1161    Transform {
1162        position: t.position * mapping.scale_factor + mapping.position_offset,
1163        rotation: (mapping.rotation_offset * swing * twist).normalize(),
1164        scale: t.scale,
1165    }
1166}
1167
1168#[derive(Debug, Clone)]
1169pub struct RetargetingData {
1170    pub mappings: Vec<BoneMapping>,
1171    pub scale_factors: HashMap<u32, f32>,
1172}
1173
1174impl RetargetingData {
1175    pub fn new() -> Self {
1176        Self { mappings: Vec::new(), scale_factors: HashMap::new() }
1177    }
1178
1179    pub fn add_mapping(&mut self, m: BoneMapping) {
1180        self.mappings.push(m);
1181    }
1182
1183    pub fn retarget_clip(&self, source: &AnimationClip) -> AnimationClip {
1184        let mut result = AnimationClip::new(&source.name, source.frame_rate, source.duration);
1185        result.looping = source.looping;
1186        for track in &source.tracks {
1187            if let Some(mapping) = self.mappings.iter().find(|m| m.source_bone == track.bone_index) {
1188                let new_keyframes = track.keyframes.iter().map(|kf| {
1189                    Keyframe {
1190                        time: kf.time,
1191                        transform: retarget_transform(&kf.transform, mapping),
1192                    }
1193                }).collect();
1194                result.tracks.push(BoneTrack {
1195                    bone_index: mapping.target_bone,
1196                    keyframes: new_keyframes,
1197                    importance: track.importance,
1198                });
1199            }
1200        }
1201        result
1202    }
1203}
1204
1205// ─── Additive Animation ───────────────────────────────────────────────────────
1206
1207/// Extract an additive layer by subtracting a reference pose.
1208pub fn extract_additive_layer(
1209    clip: &AnimationClip,
1210    reference: &ReferencePose,
1211) -> AnimationClip {
1212    let mut additive = AnimationClip::new(
1213        &format!("{}_additive", clip.name),
1214        clip.frame_rate,
1215        clip.duration,
1216    );
1217    additive.looping = clip.looping;
1218
1219    for track in &clip.tracks {
1220        let bone_idx = track.bone_index as usize;
1221        let ref_t = if bone_idx < reference.transforms.len() {
1222            reference.transforms[bone_idx]
1223        } else {
1224            Transform::identity()
1225        };
1226
1227        let new_keyframes = track.keyframes.iter().map(|kf| {
1228            let pos_offset = kf.transform.position - ref_t.position;
1229            let rot_diff = ref_t.rotation.inverse() * kf.transform.rotation;
1230            let scale_mult = Vec3::new(
1231                kf.transform.scale.x / ref_t.scale.x.max(1e-6),
1232                kf.transform.scale.y / ref_t.scale.y.max(1e-6),
1233                kf.transform.scale.z / ref_t.scale.z.max(1e-6),
1234            );
1235            Keyframe {
1236                time: kf.time,
1237                transform: Transform {
1238                    position: pos_offset,
1239                    rotation: rot_diff.normalize(),
1240                    scale: scale_mult,
1241                },
1242            }
1243        }).collect();
1244
1245        additive.tracks.push(BoneTrack {
1246            bone_index: track.bone_index,
1247            keyframes: new_keyframes,
1248            importance: track.importance,
1249        });
1250    }
1251    additive
1252}
1253
1254/// Apply an additive layer on top of a base pose.
1255pub fn apply_additive_layer(
1256    base: &Transform,
1257    additive: &Transform,
1258    weight: f32,
1259) -> Transform {
1260    let pos = base.position + additive.position * weight;
1261    let rot_add = Quat::IDENTITY.slerp(additive.rotation, weight);
1262    let rot = (base.rotation * rot_add).normalize();
1263    let scale = base.scale * additive.scale.lerp(Vec3::ONE, 1.0 - weight);
1264    Transform { position: pos, rotation: rot, scale }
1265}
1266
1267/// Sample an additive clip and apply to a pose array.
1268pub fn apply_additive_clip_to_pose(
1269    base_pose: &mut Vec<Transform>,
1270    additive_clip: &AnimationClip,
1271    time: f32,
1272    weight: f32,
1273) {
1274    for track in &additive_clip.tracks {
1275        let idx = track.bone_index as usize;
1276        if idx < base_pose.len() {
1277            if let Some(add_t) = additive_clip.sample_at(track.bone_index, time) {
1278                base_pose[idx] = apply_additive_layer(&base_pose[idx], &add_t, weight);
1279            }
1280        }
1281    }
1282}
1283
1284// ─── LOD Animation ───────────────────────────────────────────────────────────
1285
1286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1287pub enum BoneGroup {
1288    Spine,
1289    Arms,
1290    Legs,
1291    Hands,
1292    Fingers,
1293    Head,
1294    Face,
1295    Tail,
1296    Other,
1297}
1298
1299#[derive(Debug, Clone)]
1300pub struct BoneLodInfo {
1301    pub bone_index: u32,
1302    pub group: BoneGroup,
1303    pub min_lod_distance: f32,  // distance at which this bone starts being simplified
1304    pub max_lod_distance: f32,  // distance at which this bone is fully skipped
1305}
1306
1307impl BoneLodInfo {
1308    pub fn new(bone_index: u32, group: BoneGroup) -> Self {
1309        let (min_d, max_d) = match group {
1310            BoneGroup::Fingers => (5.0, 15.0),
1311            BoneGroup::Face => (8.0, 20.0),
1312            BoneGroup::Hands => (10.0, 25.0),
1313            BoneGroup::Tail => (12.0, 30.0),
1314            BoneGroup::Head => (20.0, 50.0),
1315            BoneGroup::Arms | BoneGroup::Legs => (30.0, 80.0),
1316            BoneGroup::Spine => (50.0, 120.0),
1317            BoneGroup::Other => (15.0, 40.0),
1318        };
1319        Self { bone_index, group, min_lod_distance: min_d, max_lod_distance: max_d }
1320    }
1321
1322    pub fn lod_factor(&self, distance: f32) -> f32 {
1323        if distance <= self.min_lod_distance { 1.0 }
1324        else if distance >= self.max_lod_distance { 0.0 }
1325        else {
1326            1.0 - (distance - self.min_lod_distance) / (self.max_lod_distance - self.min_lod_distance)
1327        }
1328    }
1329
1330    pub fn should_skip(&self, distance: f32) -> bool {
1331        distance >= self.max_lod_distance
1332    }
1333}
1334
1335#[derive(Debug, Clone)]
1336pub struct LodAnimationVariant {
1337    pub lod_level: u32,
1338    pub distance_threshold: f32,
1339    pub clip: AnimationClip,
1340}
1341
1342/// Create LOD variants of an animation clip.
1343pub fn create_lod_variants(
1344    clip: &AnimationClip,
1345    bone_lod_info: &[BoneLodInfo],
1346    lod_distances: &[f32], // e.g. [5.0, 15.0, 40.0, 100.0]
1347) -> Vec<LodAnimationVariant> {
1348    lod_distances.iter().enumerate().map(|(lod_idx, &distance)| {
1349        let mut lod_clip = AnimationClip::new(
1350            &format!("{}_lod{}", clip.name, lod_idx),
1351            clip.frame_rate,
1352            clip.duration,
1353        );
1354        lod_clip.looping = clip.looping;
1355
1356        for track in &clip.tracks {
1357            let lod_info = bone_lod_info.iter().find(|b| b.bone_index == track.bone_index);
1358            let skip = lod_info.map_or(false, |b| b.should_skip(distance));
1359            if skip { continue; }
1360
1361            // Apply reduction based on LOD level
1362            let lod_factor = lod_info.map_or(1.0, |b| b.lod_factor(distance));
1363            let base_eps_pos = 0.001;
1364            let base_eps_rot = 0.001;
1365            let eps_pos = base_eps_pos / lod_factor.max(0.01);
1366            let eps_rot = base_eps_rot / lod_factor.max(0.01);
1367
1368            let reduced = rdp_reduce_track(track, eps_pos, eps_rot);
1369            lod_clip.tracks.push(reduced);
1370        }
1371
1372        LodAnimationVariant {
1373            lod_level: lod_idx as u32,
1374            distance_threshold: distance,
1375            clip: lod_clip,
1376        }
1377    }).collect()
1378}
1379
1380/// Select the best LOD variant for a given distance.
1381pub fn select_lod_variant<'a>(
1382    variants: &'a [LodAnimationVariant],
1383    distance: f32,
1384) -> Option<&'a LodAnimationVariant> {
1385    // Return highest LOD that fits (largest threshold <= distance)
1386    variants.iter()
1387        .filter(|v| v.distance_threshold <= distance)
1388        .last()
1389        .or_else(|| variants.first())
1390}
1391
1392// ─── Compressed Animation Clip ────────────────────────────────────────────────
1393
1394#[derive(Debug, Clone)]
1395pub struct CompressedAnimationClip {
1396    pub name: String,
1397    pub frame_rate: f32,
1398    pub duration: f32,
1399    pub looping: bool,
1400    pub tracks: Vec<CompressedTrack>,
1401    pub original_keyframe_count: usize,
1402    pub compressed_keyframe_count: usize,
1403    pub original_byte_size: usize,
1404    pub compressed_byte_size: usize,
1405}
1406
1407impl CompressedAnimationClip {
1408    pub fn compression_ratio(&self) -> f32 {
1409        if self.compressed_byte_size == 0 { return 0.0; }
1410        self.original_byte_size as f32 / self.compressed_byte_size as f32
1411    }
1412
1413    pub fn keyframe_reduction_ratio(&self) -> f32 {
1414        if self.compressed_keyframe_count == 0 { return 0.0; }
1415        self.original_keyframe_count as f32 / self.compressed_keyframe_count as f32
1416    }
1417}
1418
1419// ─── Error Analysis ───────────────────────────────────────────────────────────
1420
1421#[derive(Debug, Clone)]
1422pub struct CompressionErrorReport {
1423    pub clip_name: String,
1424    pub per_bone_max_pos_error: Vec<(u32, f32)>,
1425    pub per_bone_max_rot_error: Vec<(u32, f32)>,
1426    pub per_bone_max_scale_error: Vec<(u32, f32)>,
1427    pub global_max_pos_error: f32,
1428    pub global_max_rot_error: f32,
1429    pub global_rms_pos_error: f32,
1430    pub global_rms_rot_error: f32,
1431    pub total_original_keyframes: usize,
1432    pub total_compressed_keyframes: usize,
1433    pub byte_size_original: usize,
1434    pub byte_size_compressed: usize,
1435}
1436
1437impl CompressionErrorReport {
1438    pub fn new(clip_name: &str) -> Self {
1439        Self {
1440            clip_name: clip_name.to_owned(),
1441            per_bone_max_pos_error: Vec::new(),
1442            per_bone_max_rot_error: Vec::new(),
1443            per_bone_max_scale_error: Vec::new(),
1444            global_max_pos_error: 0.0,
1445            global_max_rot_error: 0.0,
1446            global_rms_pos_error: 0.0,
1447            global_rms_rot_error: 0.0,
1448            total_original_keyframes: 0,
1449            total_compressed_keyframes: 0,
1450            byte_size_original: 0,
1451            byte_size_compressed: 0,
1452        }
1453    }
1454}
1455
1456pub fn compute_error_report(
1457    original: &AnimationClip,
1458    compressed: &CompressedAnimationClip,
1459) -> CompressionErrorReport {
1460    let mut report = CompressionErrorReport::new(&original.name);
1461    report.total_original_keyframes = original.total_keyframes();
1462    report.total_compressed_keyframes = compressed.tracks.iter().map(|t| t.keyframes.len()).sum();
1463    // Byte sizes: estimate
1464    report.byte_size_original = original.total_keyframes() * 40; // rough: 40 bytes per keyframe
1465    report.byte_size_compressed = compressed.tracks.iter().map(|t| t.byte_size()).sum();
1466
1467    let mut pos_sq_sum = 0.0f64;
1468    let mut rot_sq_sum = 0.0f64;
1469    let mut sample_count = 0usize;
1470
1471    for orig_track in &original.tracks {
1472        let comp_track = match compressed.tracks.iter().find(|t| t.bone_index == orig_track.bone_index) {
1473            Some(t) => t,
1474            None => continue,
1475        };
1476        let decompressed = comp_track.to_track();
1477
1478        let mut bone_max_pos = 0.0f32;
1479        let mut bone_max_rot = 0.0f32;
1480        let mut bone_max_scale = 0.0f32;
1481
1482        for orig_kf in &orig_track.keyframes {
1483            let t = orig_kf.time;
1484            // Sample the decompressed track at this time
1485            if let Some(decomp_t) = decompressed.keyframes.iter().enumerate().find_map(|(i, kf)| {
1486                if i == 0 || i == decompressed.keyframes.len() - 1 { return None; }
1487                let k0 = &decompressed.keyframes[i-1];
1488                let k1 = &decompressed.keyframes[i];
1489                if t >= k0.time && t <= k1.time {
1490                    let dt = k1.time - k0.time;
1491                    let a = if dt > 1e-9 { (t - k0.time) / dt } else { 0.0 };
1492                    Some(k0.transform.lerp(&k1.transform, a))
1493                } else { None }
1494            }) {
1495                let pe = position_error_l2(orig_kf.transform.position, decomp_t.position);
1496                let re = rotation_error_geodesic(orig_kf.transform.rotation, decomp_t.rotation);
1497                let se = scale_error_l2(orig_kf.transform.scale, decomp_t.scale);
1498                bone_max_pos = bone_max_pos.max(pe);
1499                bone_max_rot = bone_max_rot.max(re);
1500                bone_max_scale = bone_max_scale.max(se);
1501                pos_sq_sum += (pe * pe) as f64;
1502                rot_sq_sum += (re * re) as f64;
1503                sample_count += 1;
1504            }
1505        }
1506
1507        report.per_bone_max_pos_error.push((orig_track.bone_index, bone_max_pos));
1508        report.per_bone_max_rot_error.push((orig_track.bone_index, bone_max_rot));
1509        report.per_bone_max_scale_error.push((orig_track.bone_index, bone_max_scale));
1510        report.global_max_pos_error = report.global_max_pos_error.max(bone_max_pos);
1511        report.global_max_rot_error = report.global_max_rot_error.max(bone_max_rot);
1512    }
1513
1514    if sample_count > 0 {
1515        report.global_rms_pos_error = ((pos_sq_sum / sample_count as f64).sqrt()) as f32;
1516        report.global_rms_rot_error = ((rot_sq_sum / sample_count as f64).sqrt()) as f32;
1517    }
1518
1519    report
1520}
1521
1522// ─── Compression Settings ─────────────────────────────────────────────────────
1523
1524#[derive(Debug, Clone)]
1525pub struct CompressionSettings {
1526    pub pos_tolerance: f32,
1527    pub rot_tolerance_radians: f32,
1528    pub scale_tolerance: f32,
1529    pub use_rdp: bool,
1530    pub use_hermite_fitting: bool,
1531    pub use_quantization: bool,
1532    pub use_delta_compression: bool,
1533    pub tick_rate: f32,
1534    pub delta_pos_threshold: f32,
1535    pub delta_rot_threshold: f32,
1536    pub delta_scale_threshold: f32,
1537}
1538
1539impl Default for CompressionSettings {
1540    fn default() -> Self {
1541        Self {
1542            pos_tolerance: 0.001,
1543            rot_tolerance_radians: 0.001,
1544            scale_tolerance: 0.001,
1545            use_rdp: true,
1546            use_hermite_fitting: false,
1547            use_quantization: true,
1548            use_delta_compression: false,
1549            tick_rate: 30.0,
1550            delta_pos_threshold: 1e-5,
1551            delta_rot_threshold: 1e-4,
1552            delta_scale_threshold: 1e-4,
1553        }
1554    }
1555}
1556
1557// ─── The Main AnimationCompressor ────────────────────────────────────────────
1558
1559pub struct AnimationCompressor {
1560    pub settings: CompressionSettings,
1561    pub bone_lod_info: Vec<BoneLodInfo>,
1562    pub retargeting: Option<RetargetingData>,
1563}
1564
1565impl AnimationCompressor {
1566    pub fn new() -> Self {
1567        Self {
1568            settings: CompressionSettings::default(),
1569            bone_lod_info: Vec::new(),
1570            retargeting: None,
1571        }
1572    }
1573
1574    pub fn with_settings(mut self, s: CompressionSettings) -> Self {
1575        self.settings = s;
1576        self
1577    }
1578
1579    pub fn with_bone_lod(mut self, info: Vec<BoneLodInfo>) -> Self {
1580        self.bone_lod_info = info;
1581        self
1582    }
1583
1584    pub fn with_retargeting(mut self, r: RetargetingData) -> Self {
1585        self.retargeting = Some(r);
1586        self
1587    }
1588
1589    /// Compress a single clip.
1590    pub fn compress(&self, clip: &AnimationClip) -> CompressedAnimationClip {
1591        let original_keyframe_count = clip.total_keyframes();
1592        let original_byte_size = original_keyframe_count * 40;
1593
1594        let mut compressed_tracks = Vec::new();
1595
1596        for track in &clip.tracks {
1597            // Step 1: RDP reduction
1598            let reduced = if self.settings.use_rdp {
1599                rdp_reduce_track(track, self.settings.pos_tolerance, self.settings.rot_tolerance_radians)
1600            } else {
1601                track.clone()
1602            };
1603
1604            // Step 2: Hermite fitting
1605            let fitted = if self.settings.use_hermite_fitting {
1606                hermite_reduce_track(&reduced, self.settings.pos_tolerance)
1607            } else {
1608                reduced
1609            };
1610
1611            // Step 3: Quantize and store
1612            if self.settings.use_quantization {
1613                compressed_tracks.push(CompressedTrack::from_track(&fitted, self.settings.tick_rate));
1614            } else {
1615                compressed_tracks.push(CompressedTrack::from_track(&fitted, self.settings.tick_rate));
1616            }
1617        }
1618
1619        let compressed_keyframe_count = compressed_tracks.iter().map(|t| t.keyframes.len()).sum();
1620        let compressed_byte_size = compressed_tracks.iter().map(|t| t.byte_size()).sum();
1621
1622        CompressedAnimationClip {
1623            name: clip.name.clone(),
1624            frame_rate: clip.frame_rate,
1625            duration: clip.duration,
1626            looping: clip.looping,
1627            tracks: compressed_tracks,
1628            original_keyframe_count,
1629            compressed_keyframe_count,
1630            original_byte_size,
1631            compressed_byte_size,
1632        }
1633    }
1634
1635    /// Decompress a clip back to AnimationClip.
1636    pub fn decompress(&self, compressed: &CompressedAnimationClip) -> AnimationClip {
1637        let mut clip = AnimationClip::new(&compressed.name, compressed.frame_rate, compressed.duration);
1638        clip.looping = compressed.looping;
1639        for ct in &compressed.tracks {
1640            clip.tracks.push(ct.to_track());
1641        }
1642        clip
1643    }
1644
1645    /// Full error analysis.
1646    pub fn error_analysis(
1647        &self,
1648        original: &AnimationClip,
1649        compressed: &CompressedAnimationClip,
1650    ) -> CompressionErrorReport {
1651        compute_error_report(original, compressed)
1652    }
1653
1654    /// Compress many clips in one call.
1655    pub fn batch_compress(&self, clips: &[&AnimationClip]) -> Vec<CompressedAnimationClip> {
1656        clips.iter().map(|c| self.compress(c)).collect()
1657    }
1658
1659    /// Compress with full LOD generation.
1660    pub fn compress_with_lod(
1661        &self,
1662        clip: &AnimationClip,
1663        lod_distances: &[f32],
1664    ) -> Vec<CompressedAnimationClip> {
1665        let variants = create_lod_variants(clip, &self.bone_lod_info, lod_distances);
1666        variants.iter().map(|v| self.compress(&v.clip)).collect()
1667    }
1668
1669    /// Build a streaming asset from a clip.
1670    pub fn build_streaming_asset(
1671        &self,
1672        clip: &AnimationClip,
1673        chunk_size: usize,
1674    ) -> StreamingAnimationAsset {
1675        StreamingAnimationAsset::build_from_clip(clip, chunk_size)
1676    }
1677}
1678
1679// ─── Animation Pose Evaluation ───────────────────────────────────────────────
1680
1681/// Full pose at a given time, evaluating all bones.
1682pub struct PoseEvaluator {
1683    pub n_bones: usize,
1684}
1685
1686impl PoseEvaluator {
1687    pub fn new(n_bones: usize) -> Self {
1688        Self { n_bones }
1689    }
1690
1691    pub fn evaluate(&self, clip: &AnimationClip, time: f32) -> Vec<Transform> {
1692        let mut pose = vec![Transform::identity(); self.n_bones];
1693        for track in &clip.tracks {
1694            let idx = track.bone_index as usize;
1695            if idx < self.n_bones {
1696                if let Some(t) = clip.sample_at(track.bone_index, time) {
1697                    pose[idx] = t;
1698                }
1699            }
1700        }
1701        pose
1702    }
1703
1704    pub fn evaluate_compressed(&self, clip: &CompressedAnimationClip, time: f32) -> Vec<Transform> {
1705        let mut pose = vec![Transform::identity(); self.n_bones];
1706        for ct in &clip.tracks {
1707            let idx = ct.bone_index as usize;
1708            if idx >= self.n_bones || ct.keyframes.is_empty() { continue; }
1709            let decompressed = ct.to_track();
1710            // Sample at time
1711            let t_start = ct.keyframes.first().map(|k| k.time_ticks as f32 / (ct.tick_rate * 1000.0)).unwrap_or(0.0);
1712            let t_end = ct.keyframes.last().map(|k| k.time_ticks as f32 / (ct.tick_rate * 1000.0)).unwrap_or(0.0);
1713            let t_clamped = time.clamp(t_start, t_end);
1714            let kfs = &decompressed.keyframes;
1715            if kfs.len() == 1 {
1716                pose[idx] = kfs[0].transform;
1717                continue;
1718            }
1719            let seg_idx = kfs.partition_point(|kf| kf.time <= t_clamped).min(kfs.len() - 1);
1720            let seg_idx = seg_idx.max(1);
1721            let kf0 = &kfs[seg_idx - 1];
1722            let kf1 = &kfs[seg_idx];
1723            let dt = kf1.time - kf0.time;
1724            let alpha = if dt > 1e-9 { (t_clamped - kf0.time) / dt } else { 0.0 };
1725            pose[idx] = kf0.transform.lerp(&kf1.transform, alpha);
1726        }
1727        pose
1728    }
1729
1730    pub fn blend_poses(
1731        &self,
1732        pose_a: &[Transform],
1733        pose_b: &[Transform],
1734        weight: f32,
1735    ) -> Vec<Transform> {
1736        pose_a.iter().zip(pose_b.iter()).map(|(a, b)| a.lerp(b, weight)).collect()
1737    }
1738}
1739
1740// ─── Batch Processing Statistics ─────────────────────────────────────────────
1741
1742#[derive(Debug, Clone, Default)]
1743pub struct BatchCompressionStats {
1744    pub total_clips: usize,
1745    pub total_original_bytes: usize,
1746    pub total_compressed_bytes: usize,
1747    pub total_original_keyframes: usize,
1748    pub total_compressed_keyframes: usize,
1749    pub avg_compression_ratio: f32,
1750    pub max_pos_error: f32,
1751    pub max_rot_error: f32,
1752    pub per_clip: Vec<(String, f32, f32)>, // (name, ratio, max_pos_err)
1753}
1754
1755impl BatchCompressionStats {
1756    pub fn compute(
1757        originals: &[&AnimationClip],
1758        compressed: &[CompressedAnimationClip],
1759    ) -> Self {
1760        let mut stats = Self::default();
1761        stats.total_clips = originals.len();
1762        for (orig, comp) in originals.iter().zip(compressed.iter()) {
1763            stats.total_original_bytes += orig.total_keyframes() * 40;
1764            stats.total_compressed_bytes += comp.compressed_byte_size;
1765            stats.total_original_keyframes += orig.total_keyframes();
1766            stats.total_compressed_keyframes += comp.compressed_keyframe_count;
1767            let ratio = if comp.compressed_byte_size > 0 {
1768                (orig.total_keyframes() * 40) as f32 / comp.compressed_byte_size as f32
1769            } else { 0.0 };
1770            stats.per_clip.push((orig.name.clone(), ratio, 0.0));
1771        }
1772        if stats.total_clips > 0 {
1773            let sum: f32 = stats.per_clip.iter().map(|(_, r, _)| r).sum();
1774            stats.avg_compression_ratio = sum / stats.total_clips as f32;
1775        }
1776        stats
1777    }
1778}
1779
1780// ─── Additive Blend Layer System ──────────────────────────────────────────────
1781
1782#[derive(Debug, Clone)]
1783pub struct AdditiveLayer {
1784    pub name: String,
1785    pub clip: AnimationClip,
1786    pub weight: f32,
1787    pub mask: Vec<u32>, // bone indices affected
1788}
1789
1790impl AdditiveLayer {
1791    pub fn new(name: &str, clip: AnimationClip, weight: f32) -> Self {
1792        Self { name: name.to_owned(), clip, weight, mask: Vec::new() }
1793    }
1794
1795    pub fn with_mask(mut self, mask: Vec<u32>) -> Self {
1796        self.mask = mask;
1797        self
1798    }
1799
1800    pub fn is_masked(&self, bone_index: u32) -> bool {
1801        self.mask.is_empty() || self.mask.contains(&bone_index)
1802    }
1803}
1804
1805pub struct AdditiveLayerStack {
1806    pub base_pose: Vec<Transform>,
1807    pub layers: Vec<AdditiveLayer>,
1808}
1809
1810impl AdditiveLayerStack {
1811    pub fn new(n_bones: usize) -> Self {
1812        Self {
1813            base_pose: vec![Transform::identity(); n_bones],
1814            layers: Vec::new(),
1815        }
1816    }
1817
1818    pub fn push_layer(&mut self, layer: AdditiveLayer) {
1819        self.layers.push(layer);
1820    }
1821
1822    pub fn evaluate(&self, time: f32) -> Vec<Transform> {
1823        let mut pose = self.base_pose.clone();
1824        for layer in &self.layers {
1825            for (bone_idx, transform) in pose.iter_mut().enumerate() {
1826                if !layer.is_masked(bone_idx as u32) { continue; }
1827                if let Some(add_t) = layer.clip.sample_at(bone_idx as u32, time) {
1828                    *transform = apply_additive_layer(transform, &add_t, layer.weight);
1829                }
1830            }
1831        }
1832        pose
1833    }
1834}
1835
1836// ─── Curve Channel Compression ───────────────────────────────────────────────
1837
1838#[derive(Debug, Clone, Copy, PartialEq)]
1839pub enum CurveChannel {
1840    PosX, PosY, PosZ,
1841    RotW, RotX, RotY, RotZ,
1842    ScaleX, ScaleY, ScaleZ,
1843}
1844
1845#[derive(Debug, Clone)]
1846pub struct ScalarKeyframe {
1847    pub time: f32,
1848    pub value: f32,
1849}
1850
1851#[derive(Debug, Clone)]
1852pub struct ScalarCurve {
1853    pub channel: CurveChannel,
1854    pub keyframes: Vec<ScalarKeyframe>,
1855}
1856
1857impl ScalarCurve {
1858    pub fn new(channel: CurveChannel) -> Self {
1859        Self { channel, keyframes: Vec::new() }
1860    }
1861
1862    pub fn sample(&self, t: f32) -> f32 {
1863        if self.keyframes.is_empty() { return 0.0; }
1864        if self.keyframes.len() == 1 { return self.keyframes[0].value; }
1865        let idx = self.keyframes.partition_point(|kf| kf.time <= t);
1866        if idx == 0 { return self.keyframes[0].value; }
1867        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().value; }
1868        let k0 = &self.keyframes[idx - 1];
1869        let k1 = &self.keyframes[idx];
1870        let dt = k1.time - k0.time;
1871        let alpha = if dt > 1e-9 { (t - k0.time) / dt } else { 0.0 };
1872        k0.value + (k1.value - k0.value) * alpha
1873    }
1874
1875    pub fn rdp_reduce(&self, epsilon: f32) -> ScalarCurve {
1876        if self.keyframes.len() <= 2 { return self.clone(); }
1877        let n = self.keyframes.len();
1878        let mut keep = vec![false; n];
1879        keep[0] = true;
1880        keep[n-1] = true;
1881        scalar_rdp(&self.keyframes, 0, n-1, epsilon, &mut keep);
1882        ScalarCurve {
1883            channel: self.channel,
1884            keyframes: self.keyframes.iter().enumerate()
1885                .filter(|(i, _)| keep[*i])
1886                .map(|(_, kf)| kf.clone())
1887                .collect(),
1888        }
1889    }
1890}
1891
1892fn scalar_rdp(kfs: &[ScalarKeyframe], start: usize, end: usize, eps: f32, keep: &mut Vec<bool>) {
1893    if end - start < 2 { return; }
1894    let t0 = kfs[start].time;
1895    let t1 = kfs[end].time;
1896    let v0 = kfs[start].value;
1897    let v1 = kfs[end].value;
1898    let dt = t1 - t0;
1899
1900    let mut max_dist = 0.0f32;
1901    let mut max_idx = start + 1;
1902    for i in (start+1)..end {
1903        let alpha = if dt > 1e-9 { (kfs[i].time - t0) / dt } else { 0.0 };
1904        let interp = v0 + (v1 - v0) * alpha;
1905        let d = (kfs[i].value - interp).abs();
1906        if d > max_dist { max_dist = d; max_idx = i; }
1907    }
1908
1909    if max_dist > eps {
1910        keep[max_idx] = true;
1911        scalar_rdp(kfs, start, max_idx, eps, keep);
1912        scalar_rdp(kfs, max_idx, end, eps, keep);
1913    }
1914}
1915
1916/// Decompose a BoneTrack into 10 scalar curves (one per channel).
1917pub fn decompose_track_to_scalar_curves(track: &BoneTrack) -> Vec<ScalarCurve> {
1918    let channels = [
1919        CurveChannel::PosX, CurveChannel::PosY, CurveChannel::PosZ,
1920        CurveChannel::RotW, CurveChannel::RotX, CurveChannel::RotY, CurveChannel::RotZ,
1921        CurveChannel::ScaleX, CurveChannel::ScaleY, CurveChannel::ScaleZ,
1922    ];
1923    channels.iter().map(|&ch| {
1924        let mut curve = ScalarCurve::new(ch);
1925        for kf in &track.keyframes {
1926            let value = match ch {
1927                CurveChannel::PosX => kf.transform.position.x,
1928                CurveChannel::PosY => kf.transform.position.y,
1929                CurveChannel::PosZ => kf.transform.position.z,
1930                CurveChannel::RotW => kf.transform.rotation.w,
1931                CurveChannel::RotX => kf.transform.rotation.x,
1932                CurveChannel::RotY => kf.transform.rotation.y,
1933                CurveChannel::RotZ => kf.transform.rotation.z,
1934                CurveChannel::ScaleX => kf.transform.scale.x,
1935                CurveChannel::ScaleY => kf.transform.scale.y,
1936                CurveChannel::ScaleZ => kf.transform.scale.z,
1937            };
1938            curve.keyframes.push(ScalarKeyframe { time: kf.time, value });
1939        }
1940        curve
1941    }).collect()
1942}
1943
1944// ─── Quaternion Curve Averaging ───────────────────────────────────────────────
1945
1946/// Average N quaternions (weighted). Used in blend tree base pose computation.
1947pub fn average_quaternions(quats: &[(Quat, f32)]) -> Quat {
1948    if quats.is_empty() { return Quat::IDENTITY; }
1949    if quats.len() == 1 { return quats[0].0; }
1950    let mut result = quats[0].0;
1951    let total_w: f32 = quats.iter().map(|(_, w)| w).sum();
1952    if total_w < 1e-9 { return Quat::IDENTITY; }
1953    let mut acc_w = quats[0].1 / total_w;
1954    for &(q, w) in quats.iter().skip(1) {
1955        let t = (w / total_w) / (acc_w + w / total_w).max(1e-9);
1956        result = result.slerp(q, t);
1957        acc_w += w / total_w;
1958    }
1959    result.normalize()
1960}
1961
1962// ─── Binary Serialization Helpers ────────────────────────────────────────────
1963
1964pub struct BitWriter {
1965    pub data: Vec<u8>,
1966    pub bit_pos: usize,
1967}
1968
1969impl BitWriter {
1970    pub fn new() -> Self {
1971        Self { data: Vec::new(), bit_pos: 0 }
1972    }
1973
1974    pub fn write_bits(&mut self, value: u64, n_bits: usize) {
1975        for i in 0..n_bits {
1976            let bit = ((value >> (n_bits - 1 - i)) & 1) as u8;
1977            let byte_idx = self.bit_pos / 8;
1978            let bit_offset = 7 - (self.bit_pos % 8);
1979            if byte_idx >= self.data.len() {
1980                self.data.push(0);
1981            }
1982            self.data[byte_idx] |= bit << bit_offset;
1983            self.bit_pos += 1;
1984        }
1985    }
1986
1987    pub fn write_u16(&mut self, v: u16) { self.write_bits(v as u64, 16); }
1988    pub fn write_u8(&mut self, v: u8) { self.write_bits(v as u64, 8); }
1989    pub fn write_i16(&mut self, v: i16) { self.write_bits(v as u16 as u64, 16); }
1990    pub fn write_u32(&mut self, v: u32) { self.write_bits(v as u64, 32); }
1991
1992    pub fn byte_size(&self) -> usize {
1993        (self.bit_pos + 7) / 8
1994    }
1995}
1996
1997pub struct BitReader<'a> {
1998    pub data: &'a [u8],
1999    pub bit_pos: usize,
2000}
2001
2002impl<'a> BitReader<'a> {
2003    pub fn new(data: &'a [u8]) -> Self {
2004        Self { data, bit_pos: 0 }
2005    }
2006
2007    pub fn read_bits(&mut self, n_bits: usize) -> u64 {
2008        let mut value = 0u64;
2009        for i in 0..n_bits {
2010            let byte_idx = self.bit_pos / 8;
2011            let bit_offset = 7 - (self.bit_pos % 8);
2012            if byte_idx >= self.data.len() { break; }
2013            let bit = ((self.data[byte_idx] >> bit_offset) & 1) as u64;
2014            value |= bit << (n_bits - 1 - i);
2015            self.bit_pos += 1;
2016        }
2017        value
2018    }
2019
2020    pub fn read_u16(&mut self) -> u16 { self.read_bits(16) as u16 }
2021    pub fn read_u8(&mut self) -> u8 { self.read_bits(8) as u8 }
2022    pub fn read_i16(&mut self) -> i16 { self.read_bits(16) as i16 }
2023    pub fn read_u32(&mut self) -> u32 { self.read_bits(32) as u32 }
2024}
2025
2026/// Serialize a CompressedTrack to bytes.
2027pub fn serialize_compressed_track(track: &CompressedTrack) -> Vec<u8> {
2028    let mut w = BitWriter::new();
2029    w.write_u32(track.bone_index);
2030    // Bounds: min and max as f32 (6 floats)
2031    let bounds_data: [f32; 6] = [
2032        track.pos_bounds.min.x, track.pos_bounds.min.y, track.pos_bounds.min.z,
2033        track.pos_bounds.max.x, track.pos_bounds.max.y, track.pos_bounds.max.z,
2034    ];
2035    for &f in &bounds_data {
2036        w.write_u32(f.to_bits());
2037    }
2038    w.write_u32(track.tick_rate.to_bits());
2039    w.write_u32(track.keyframes.len() as u32);
2040    for kf in &track.keyframes {
2041        w.write_u32(kf.time_ticks);
2042        w.write_u16(kf.position[0]);
2043        w.write_u16(kf.position[1]);
2044        w.write_u16(kf.position[2]);
2045        w.write_u8(kf.rotation.largest_component);
2046        w.write_i16(kf.rotation.components[0]);
2047        w.write_i16(kf.rotation.components[1]);
2048        w.write_i16(kf.rotation.components[2]);
2049        w.write_u8(kf.scale[0]);
2050        w.write_u8(kf.scale[1]);
2051        w.write_u8(kf.scale[2]);
2052    }
2053    w.data
2054}
2055
2056pub fn deserialize_compressed_track(data: &[u8]) -> CompressedTrack {
2057    let mut r = BitReader::new(data);
2058    let bone_index = r.read_u32();
2059    let min_x = f32::from_bits(r.read_u32());
2060    let min_y = f32::from_bits(r.read_u32());
2061    let min_z = f32::from_bits(r.read_u32());
2062    let max_x = f32::from_bits(r.read_u32());
2063    let max_y = f32::from_bits(r.read_u32());
2064    let max_z = f32::from_bits(r.read_u32());
2065    let tick_rate = f32::from_bits(r.read_u32());
2066    let n_kf = r.read_u32() as usize;
2067    let mut keyframes = Vec::with_capacity(n_kf);
2068    for _ in 0..n_kf {
2069        let time_ticks = r.read_u32();
2070        let pos = [r.read_u16(), r.read_u16(), r.read_u16()];
2071        let largest_component = r.read_u8();
2072        let c0 = r.read_i16();
2073        let c1 = r.read_i16();
2074        let c2 = r.read_i16();
2075        let sc = [r.read_u8(), r.read_u8(), r.read_u8()];
2076        keyframes.push(CompressedKeyframe {
2077            time_ticks,
2078            position: pos,
2079            rotation: CompressedQuat { largest_component, components: [c0, c1, c2] },
2080            scale: sc,
2081        });
2082    }
2083    CompressedTrack {
2084        bone_index,
2085        pos_bounds: PositionBounds {
2086            min: Vec3::new(min_x, min_y, min_z),
2087            max: Vec3::new(max_x, max_y, max_z),
2088        },
2089        tick_rate,
2090        keyframes,
2091    }
2092}
2093
2094// ─── Animation Clip Registry ─────────────────────────────────────────────────
2095
2096pub struct AnimationRegistry {
2097    pub clips: HashMap<String, AnimationClip>,
2098    pub compressed: HashMap<String, CompressedAnimationClip>,
2099    pub streaming: HashMap<String, StreamingAnimationAsset>,
2100}
2101
2102impl AnimationRegistry {
2103    pub fn new() -> Self {
2104        Self {
2105            clips: HashMap::new(),
2106            compressed: HashMap::new(),
2107            streaming: HashMap::new(),
2108        }
2109    }
2110
2111    pub fn register(&mut self, clip: AnimationClip) {
2112        self.clips.insert(clip.name.clone(), clip);
2113    }
2114
2115    pub fn compress_all(&mut self, compressor: &AnimationCompressor) {
2116        let names: Vec<String> = self.clips.keys().cloned().collect();
2117        for name in names {
2118            if let Some(clip) = self.clips.get(&name) {
2119                let c = compressor.compress(clip);
2120                self.compressed.insert(name, c);
2121            }
2122        }
2123    }
2124
2125    pub fn build_streaming_all(&mut self, compressor: &AnimationCompressor, chunk_size: usize) {
2126        let names: Vec<String> = self.clips.keys().cloned().collect();
2127        for name in names {
2128            if let Some(clip) = self.clips.get(&name) {
2129                let s = compressor.build_streaming_asset(clip, chunk_size);
2130                self.streaming.insert(name, s);
2131            }
2132        }
2133    }
2134
2135    pub fn get_clip(&self, name: &str) -> Option<&AnimationClip> {
2136        self.clips.get(name)
2137    }
2138
2139    pub fn get_compressed(&self, name: &str) -> Option<&CompressedAnimationClip> {
2140        self.compressed.get(name)
2141    }
2142}
2143
2144// ─── Compression Pipeline ─────────────────────────────────────────────────────
2145
2146pub struct CompressionPipeline {
2147    pub compressor: AnimationCompressor,
2148    pub registry: AnimationRegistry,
2149    pub stats: BatchCompressionStats,
2150}
2151
2152impl CompressionPipeline {
2153    pub fn new() -> Self {
2154        Self {
2155            compressor: AnimationCompressor::new(),
2156            registry: AnimationRegistry::new(),
2157            stats: BatchCompressionStats::default(),
2158        }
2159    }
2160
2161    pub fn add_clip(&mut self, clip: AnimationClip) {
2162        self.registry.register(clip);
2163    }
2164
2165    pub fn run(&mut self) {
2166        self.registry.compress_all(&self.compressor);
2167        let originals: Vec<&AnimationClip> = self.registry.clips.values().collect();
2168        let compressed: Vec<&CompressedAnimationClip> = self.registry.compressed.values().collect();
2169        let orig_refs: Vec<&AnimationClip> = originals.iter().map(|c| *c).collect();
2170        let comp_vals: Vec<CompressedAnimationClip> = compressed.iter().map(|c| (*c).clone()).collect();
2171        self.stats = BatchCompressionStats::compute(&orig_refs, &comp_vals);
2172    }
2173
2174    pub fn report(&self) -> String {
2175        format!(
2176            "Compression pipeline: {} clips, {:.2}x avg ratio, {}/{} keyframes",
2177            self.stats.total_clips,
2178            self.stats.avg_compression_ratio,
2179            self.stats.total_compressed_keyframes,
2180            self.stats.total_original_keyframes,
2181        )
2182    }
2183}
2184
2185// ─── Procedural Animation Helpers ────────────────────────────────────────────
2186
2187/// Simple spring damper for procedural animation.
2188#[derive(Debug, Clone)]
2189pub struct SpringDamper {
2190    pub position: Vec3,
2191    pub velocity: Vec3,
2192    pub stiffness: f32,
2193    pub damping: f32,
2194}
2195
2196impl SpringDamper {
2197    pub fn new(stiffness: f32, damping: f32) -> Self {
2198        Self {
2199            position: Vec3::ZERO,
2200            velocity: Vec3::ZERO,
2201            stiffness,
2202            damping,
2203        }
2204    }
2205
2206    pub fn update(&mut self, target: Vec3, dt: f32) -> Vec3 {
2207        let force = (target - self.position) * self.stiffness - self.velocity * self.damping;
2208        self.velocity += force * dt;
2209        self.position += self.velocity * dt;
2210        self.position
2211    }
2212}
2213
2214/// IK two-bone solver.
2215pub fn ik_two_bone(
2216    root: Vec3,
2217    mid: Vec3,
2218    end: Vec3,
2219    target: Vec3,
2220    pole: Vec3,
2221    upper_len: f32,
2222    lower_len: f32,
2223) -> (Quat, Quat) {
2224    let total_len = upper_len + lower_len;
2225    let to_target = target - root;
2226    let target_dist = to_target.length().min(total_len * 0.9999);
2227
2228    // Law of cosines to find bend angle
2229    let cos_a = (upper_len * upper_len + target_dist * target_dist - lower_len * lower_len)
2230        / (2.0 * upper_len * target_dist + 1e-9);
2231    let cos_a = cos_a.clamp(-1.0, 1.0);
2232    let angle_a = cos_a.acos();
2233
2234    // Direction to target from root
2235    let dir_to_target = if to_target.length() > 1e-6 { to_target.normalize() } else { Vec3::Y };
2236
2237    // Pole vector to determine bend direction
2238    let pole_dir = (pole - root).normalize();
2239    let perp = dir_to_target.cross(pole_dir);
2240    let bend_dir = if perp.length() > 1e-6 {
2241        perp.normalize().cross(dir_to_target).normalize()
2242    } else {
2243        Vec3::Z
2244    };
2245
2246    let mid_offset = dir_to_target * (upper_len * cos_a) + bend_dir * (upper_len * angle_a.sin());
2247    let new_mid = root + mid_offset;
2248
2249    // Compute rotations
2250    let upper_rot = Quat::from_rotation_arc(Vec3::Y, (new_mid - root).normalize());
2251    let lower_dir = (target - new_mid).normalize();
2252    let lower_rot = Quat::from_rotation_arc((new_mid - root).normalize(), lower_dir);
2253
2254    (upper_rot, lower_rot)
2255}
2256
2257// ─── Frame Range Operations ───────────────────────────────────────────────────
2258
2259/// Extract a sub-range of an animation clip.
2260pub fn extract_frame_range(
2261    clip: &AnimationClip,
2262    t_start: f32,
2263    t_end: f32,
2264) -> AnimationClip {
2265    let duration = t_end - t_start;
2266    let mut result = AnimationClip::new(
2267        &format!("{}_range_{:.2}_{:.2}", clip.name, t_start, t_end),
2268        clip.frame_rate,
2269        duration.max(0.0),
2270    );
2271    result.looping = clip.looping;
2272    for track in &clip.tracks {
2273        let kfs: Vec<Keyframe> = track.keyframes.iter()
2274            .filter(|kf| kf.time >= t_start && kf.time <= t_end)
2275            .map(|kf| Keyframe { time: kf.time - t_start, transform: kf.transform })
2276            .collect();
2277        if !kfs.is_empty() {
2278            result.tracks.push(BoneTrack {
2279                bone_index: track.bone_index,
2280                keyframes: kfs,
2281                importance: track.importance,
2282            });
2283        }
2284    }
2285    result
2286}
2287
2288/// Mirror an animation around the YZ plane (flip left/right).
2289pub fn mirror_animation(clip: &AnimationClip, bone_mirror_map: &HashMap<u32, u32>) -> AnimationClip {
2290    let mut mirrored = AnimationClip::new(
2291        &format!("{}_mirror", clip.name),
2292        clip.frame_rate,
2293        clip.duration,
2294    );
2295    mirrored.looping = clip.looping;
2296    for track in &clip.tracks {
2297        let target_bone = *bone_mirror_map.get(&track.bone_index).unwrap_or(&track.bone_index);
2298        let new_kfs: Vec<Keyframe> = track.keyframes.iter().map(|kf| {
2299            let mut pos = kf.transform.position;
2300            pos.x = -pos.x; // Mirror X
2301            // Mirror rotation: flip around YZ plane
2302            let rot = kf.transform.rotation;
2303            let mirrored_rot = Quat::from_xyzw(-rot.x, rot.y, rot.z, -rot.w).normalize();
2304            Keyframe {
2305                time: kf.time,
2306                transform: Transform { position: pos, rotation: mirrored_rot, scale: kf.transform.scale },
2307            }
2308        }).collect();
2309        mirrored.tracks.push(BoneTrack {
2310            bone_index: target_bone,
2311            keyframes: new_kfs,
2312            importance: track.importance,
2313        });
2314    }
2315    mirrored
2316}
2317
2318// ─── Keyframe Reduction Statistics ───────────────────────────────────────────
2319
2320#[derive(Debug, Clone)]
2321pub struct ReductionStats {
2322    pub original_count: usize,
2323    pub reduced_count: usize,
2324    pub reduction_percent: f32,
2325}
2326
2327impl ReductionStats {
2328    pub fn compute(original: &BoneTrack, reduced: &BoneTrack) -> Self {
2329        let orig = original.keyframes.len();
2330        let red = reduced.keyframes.len();
2331        let pct = if orig > 0 { (1.0 - red as f32 / orig as f32) * 100.0 } else { 0.0 };
2332        Self { original_count: orig, reduced_count: red, reduction_percent: pct }
2333    }
2334}
2335
2336// ─── Twist/Swing Decomposition for Retargeting ───────────────────────────────
2337
2338/// Full twist/swing retargeting for a track.
2339pub fn retarget_track_swing_twist(
2340    track: &BoneTrack,
2341    mapping: &BoneMapping,
2342    twist_axis: Vec3,
2343) -> BoneTrack {
2344    let new_kfs: Vec<Keyframe> = track.keyframes.iter().map(|kf| {
2345        let (swing, twist) = swing_twist_decompose(kf.transform.rotation, twist_axis);
2346        // Apply offset to swing only (preserve twist)
2347        let new_rot = (mapping.rotation_offset * swing * twist).normalize();
2348        Keyframe {
2349            time: kf.time,
2350            transform: Transform {
2351                position: kf.transform.position * mapping.scale_factor + mapping.position_offset,
2352                rotation: new_rot,
2353                scale: kf.transform.scale,
2354            },
2355        }
2356    }).collect();
2357    BoneTrack {
2358        bone_index: mapping.target_bone,
2359        keyframes: new_kfs,
2360        importance: track.importance,
2361    }
2362}
2363
2364// ─── Animation Event System ───────────────────────────────────────────────────
2365
2366#[derive(Debug, Clone)]
2367pub struct AnimationEvent {
2368    pub time: f32,
2369    pub name: String,
2370    pub params: HashMap<String, f32>,
2371}
2372
2373impl AnimationEvent {
2374    pub fn new(time: f32, name: &str) -> Self {
2375        Self { time, name: name.to_owned(), params: HashMap::new() }
2376    }
2377
2378    pub fn with_param(mut self, key: &str, value: f32) -> Self {
2379        self.params.insert(key.to_owned(), value);
2380        self
2381    }
2382}
2383
2384pub struct AnimationEventTrack {
2385    pub events: Vec<AnimationEvent>,
2386}
2387
2388impl AnimationEventTrack {
2389    pub fn new() -> Self {
2390        Self { events: Vec::new() }
2391    }
2392
2393    pub fn add(&mut self, event: AnimationEvent) {
2394        self.events.push(event);
2395        self.events.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
2396    }
2397
2398    pub fn events_in_range(&self, t_start: f32, t_end: f32) -> Vec<&AnimationEvent> {
2399        self.events.iter().filter(|e| e.time >= t_start && e.time < t_end).collect()
2400    }
2401}
2402
2403// ─── Compression Quality Presets ─────────────────────────────────────────────
2404
2405pub fn preset_lossless() -> CompressionSettings {
2406    CompressionSettings {
2407        pos_tolerance: 0.0,
2408        rot_tolerance_radians: 0.0,
2409        scale_tolerance: 0.0,
2410        use_rdp: false,
2411        use_hermite_fitting: false,
2412        use_quantization: false,
2413        use_delta_compression: false,
2414        ..Default::default()
2415    }
2416}
2417
2418pub fn preset_high_quality() -> CompressionSettings {
2419    CompressionSettings {
2420        pos_tolerance: 0.0005,
2421        rot_tolerance_radians: 0.0005,
2422        scale_tolerance: 0.001,
2423        use_rdp: true,
2424        use_hermite_fitting: true,
2425        use_quantization: true,
2426        use_delta_compression: false,
2427        ..Default::default()
2428    }
2429}
2430
2431pub fn preset_medium_quality() -> CompressionSettings {
2432    CompressionSettings {
2433        pos_tolerance: 0.002,
2434        rot_tolerance_radians: 0.002,
2435        scale_tolerance: 0.005,
2436        use_rdp: true,
2437        use_hermite_fitting: false,
2438        use_quantization: true,
2439        use_delta_compression: true,
2440        ..Default::default()
2441    }
2442}
2443
2444pub fn preset_low_quality() -> CompressionSettings {
2445    CompressionSettings {
2446        pos_tolerance: 0.01,
2447        rot_tolerance_radians: 0.01,
2448        scale_tolerance: 0.02,
2449        use_rdp: true,
2450        use_hermite_fitting: false,
2451        use_quantization: true,
2452        use_delta_compression: true,
2453        ..Default::default()
2454    }
2455}
2456
2457// ─── Multi-pass Compression ───────────────────────────────────────────────────
2458
2459/// Multi-pass compressor: tries progressively tighter compression until error budget is met.
2460pub struct AdaptiveCompressor {
2461    pub target_max_pos_error: f32,
2462    pub target_max_rot_error: f32,
2463    pub n_passes: usize,
2464}
2465
2466impl AdaptiveCompressor {
2467    pub fn new(target_pos: f32, target_rot: f32) -> Self {
2468        Self { target_max_pos_error: target_pos, target_max_rot_error: target_rot, n_passes: 8 }
2469    }
2470
2471    pub fn compress(&self, clip: &AnimationClip) -> CompressedAnimationClip {
2472        let mut lo = 0.0f32;
2473        let mut hi = 0.1f32;
2474        let mut best: Option<CompressedAnimationClip> = None;
2475
2476        for _ in 0..self.n_passes {
2477            let mid = (lo + hi) / 2.0;
2478            let settings = CompressionSettings {
2479                pos_tolerance: mid,
2480                rot_tolerance_radians: mid * 2.0,
2481                use_rdp: true,
2482                use_quantization: true,
2483                ..Default::default()
2484            };
2485            let compressor = AnimationCompressor::new().with_settings(settings);
2486            let compressed = compressor.compress(clip);
2487            let report = compute_error_report(clip, &compressed);
2488
2489            if report.global_max_pos_error <= self.target_max_pos_error
2490                && report.global_max_rot_error <= self.target_max_rot_error
2491            {
2492                best = Some(compressed);
2493                lo = mid; // Can compress more aggressively
2494            } else {
2495                hi = mid; // Need less compression
2496            }
2497        }
2498
2499        best.unwrap_or_else(|| {
2500            let compressor = AnimationCompressor::new();
2501            compressor.compress(clip)
2502        })
2503    }
2504}
2505
2506// ─── Velocity-based Importance ───────────────────────────────────────────────
2507
2508/// Compute per-bone importance based on motion velocity.
2509pub fn compute_bone_importance_from_velocity(track: &BoneTrack) -> f32 {
2510    if track.keyframes.len() < 2 { return 0.5; }
2511    let mut total_vel = 0.0f32;
2512    for i in 1..track.keyframes.len() {
2513        let dt = track.keyframes[i].time - track.keyframes[i-1].time;
2514        if dt < 1e-9 { continue; }
2515        let dp = (track.keyframes[i].transform.position - track.keyframes[i-1].transform.position).length();
2516        let dr = rotation_error_geodesic(
2517            track.keyframes[i].transform.rotation,
2518            track.keyframes[i-1].transform.rotation,
2519        );
2520        total_vel += dp / dt + dr / dt * 0.1;
2521    }
2522    let avg_vel = total_vel / (track.keyframes.len() - 1) as f32;
2523    // Normalize to [0,1] heuristically
2524    (avg_vel / 10.0).min(1.0)
2525}
2526
2527// ─── In-place Track Optimization ─────────────────────────────────────────────
2528
2529/// Remove duplicate consecutive keyframes (same value).
2530pub fn dedup_keyframes(track: &mut BoneTrack, eps: f32) {
2531    if track.keyframes.len() < 2 { return; }
2532    let mut keep = vec![true; track.keyframes.len()];
2533    keep[0] = true;
2534    keep[track.keyframes.len() - 1] = true;
2535    for i in 1..track.keyframes.len() - 1 {
2536        let prev = &track.keyframes[i-1];
2537        let curr = &track.keyframes[i];
2538        let pos_same = position_error_l2(prev.transform.position, curr.transform.position) < eps;
2539        let rot_same = rotation_error_geodesic(prev.transform.rotation, curr.transform.rotation) < eps;
2540        let scale_same = scale_error_l2(prev.transform.scale, curr.transform.scale) < eps;
2541        if pos_same && rot_same && scale_same {
2542            keep[i] = false;
2543        }
2544    }
2545    track.keyframes = track.keyframes.iter().enumerate()
2546        .filter(|(i, _)| keep[*i])
2547        .map(|(_, kf)| kf.clone())
2548        .collect();
2549}
2550
2551/// Sort keyframes by time (in case they're out of order).
2552pub fn sort_keyframes(track: &mut BoneTrack) {
2553    track.keyframes.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
2554}
2555
2556/// Normalize all quaternions in a track.
2557pub fn normalize_track_rotations(track: &mut BoneTrack) {
2558    for kf in &mut track.keyframes {
2559        kf.transform.rotation = kf.transform.rotation.normalize();
2560    }
2561}
2562
2563/// Ensure quaternion continuity (no sign flips).
2564pub fn fix_quaternion_continuity(track: &mut BoneTrack) {
2565    for i in 1..track.keyframes.len() {
2566        let prev = track.keyframes[i-1].transform.rotation;
2567        let curr = track.keyframes[i].transform.rotation;
2568        if prev.dot(curr) < 0.0 {
2569            track.keyframes[i].transform.rotation = Quat::from_xyzw(
2570                -curr.x, -curr.y, -curr.z, -curr.w
2571            );
2572        }
2573    }
2574}
2575
2576// ─── Clip Stitching ───────────────────────────────────────────────────────────
2577
2578/// Concatenate two clips end-to-end with optional cross-fade.
2579pub fn stitch_clips(
2580    clip_a: &AnimationClip,
2581    clip_b: &AnimationClip,
2582    crossfade_duration: f32,
2583) -> AnimationClip {
2584    let total_duration = clip_a.duration + clip_b.duration - crossfade_duration;
2585    let mut result = AnimationClip::new(
2586        &format!("{}_{}", clip_a.name, clip_b.name),
2587        clip_a.frame_rate,
2588        total_duration,
2589    );
2590
2591    let all_bones: HashSet<u32> = clip_a.tracks.iter().map(|t| t.bone_index)
2592        .chain(clip_b.tracks.iter().map(|t| t.bone_index))
2593        .collect();
2594
2595    let offset = clip_a.duration - crossfade_duration;
2596
2597    for &bone in &all_bones {
2598        let mut new_kfs: Vec<Keyframe> = Vec::new();
2599
2600        // From clip A
2601        if let Some(track_a) = clip_a.tracks.iter().find(|t| t.bone_index == bone) {
2602            for kf in &track_a.keyframes {
2603                new_kfs.push(kf.clone());
2604            }
2605        }
2606
2607        // From clip B (offset by A's duration minus crossfade)
2608        if let Some(track_b) = clip_b.tracks.iter().find(|t| t.bone_index == bone) {
2609            for kf in &track_b.keyframes {
2610                let t = kf.time + offset;
2611                // During crossfade, interpolate
2612                if kf.time < crossfade_duration {
2613                    let alpha = kf.time / crossfade_duration.max(1e-9);
2614                    if let Some(a_t) = clip_a.sample_at(bone, clip_a.duration - crossfade_duration + kf.time) {
2615                        let blended = a_t.lerp(&kf.transform, alpha);
2616                        new_kfs.push(Keyframe { time: t, transform: blended });
2617                    } else {
2618                        new_kfs.push(Keyframe { time: t, transform: kf.transform });
2619                    }
2620                } else {
2621                    new_kfs.push(Keyframe { time: t, transform: kf.transform });
2622                }
2623            }
2624        }
2625
2626        new_kfs.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
2627        new_kfs.dedup_by(|a, b| (a.time - b.time).abs() < 1e-6);
2628
2629        result.tracks.push(BoneTrack {
2630            bone_index: bone,
2631            keyframes: new_kfs,
2632            importance: 1.0,
2633        });
2634    }
2635    result
2636}
2637
2638// ─── Keyframe Resampling ──────────────────────────────────────────────────────
2639
2640/// Resample an animation to a fixed frame rate.
2641pub fn resample_clip(clip: &AnimationClip, new_frame_rate: f32) -> AnimationClip {
2642    let dt = 1.0 / new_frame_rate;
2643    let n_frames = (clip.duration * new_frame_rate).ceil() as usize + 1;
2644    let mut result = AnimationClip::new(
2645        &format!("{}_resampled_{}", clip.name, new_frame_rate as u32),
2646        new_frame_rate,
2647        clip.duration,
2648    );
2649    result.looping = clip.looping;
2650
2651    for track in &clip.tracks {
2652        let mut new_kfs = Vec::with_capacity(n_frames);
2653        for fi in 0..n_frames {
2654            let t = (fi as f32 * dt).min(clip.duration);
2655            if let Some(tf) = clip.sample_at(track.bone_index, t) {
2656                new_kfs.push(Keyframe { time: t, transform: tf });
2657            }
2658        }
2659        result.tracks.push(BoneTrack {
2660            bone_index: track.bone_index,
2661            keyframes: new_kfs,
2662            importance: track.importance,
2663        });
2664    }
2665    result
2666}
2667
2668// ─── Blend Tree Evaluation ────────────────────────────────────────────────────
2669
2670/// Evaluate a blend node at a given time, producing a pose.
2671pub fn evaluate_blend_node(node: &BlendNode, time: f32, n_bones: usize) -> Vec<Transform> {
2672    if let Some(clip) = &node.clip {
2673        let evaluator = PoseEvaluator::new(n_bones);
2674        return evaluator.evaluate(clip, time);
2675    }
2676
2677    if node.children.is_empty() {
2678        return vec![Transform::identity(); n_bones];
2679    }
2680
2681    let total_weight: f32 = node.blend_weights.iter().sum();
2682    if total_weight < 1e-9 {
2683        return vec![Transform::identity(); n_bones];
2684    }
2685
2686    let mut result: Vec<Transform> = vec![Transform::identity(); n_bones];
2687    let mut accumulated_weight = 0.0f32;
2688
2689    for (child, &weight) in node.children.iter().zip(node.blend_weights.iter()) {
2690        let child_pose = evaluate_blend_node(child, time, n_bones);
2691        let norm_weight = weight / total_weight;
2692        let t = norm_weight / (accumulated_weight + norm_weight).max(1e-9);
2693        for (r, c) in result.iter_mut().zip(child_pose.iter()) {
2694            *r = r.lerp(c, t);
2695        }
2696        accumulated_weight += norm_weight;
2697    }
2698    result
2699}
2700
2701// ─── Mesh Skinning Pose ───────────────────────────────────────────────────────
2702
2703#[derive(Debug, Clone)]
2704pub struct SkeletonPose {
2705    pub local_transforms: Vec<Transform>,
2706    pub world_transforms: Vec<Transform>,
2707    pub parent_indices: Vec<Option<usize>>,
2708}
2709
2710impl SkeletonPose {
2711    pub fn new(n_bones: usize, parent_indices: Vec<Option<usize>>) -> Self {
2712        Self {
2713            local_transforms: vec![Transform::identity(); n_bones],
2714            world_transforms: vec![Transform::identity(); n_bones],
2715            parent_indices,
2716        }
2717    }
2718
2719    pub fn compute_world_transforms(&mut self) {
2720        let n = self.local_transforms.len();
2721        for i in 0..n {
2722            let local = self.local_transforms[i];
2723            self.world_transforms[i] = if let Some(parent) = self.parent_indices[i] {
2724                let parent_world = self.world_transforms[parent];
2725                let local_mat = local.to_mat4();
2726                let parent_mat = parent_world.to_mat4();
2727                let world_mat = parent_mat * local_mat;
2728                let (scale, rot, pos) = decompose_mat4(world_mat);
2729                Transform { position: pos, rotation: rot, scale }
2730            } else {
2731                local
2732            };
2733        }
2734    }
2735
2736    pub fn to_skinning_matrices(&self, inverse_bind_poses: &[Mat4]) -> Vec<Mat4> {
2737        self.world_transforms.iter().zip(inverse_bind_poses.iter()).map(|(world, ibp)| {
2738            world.to_mat4() * *ibp
2739        }).collect()
2740    }
2741}
2742
2743fn decompose_mat4(m: Mat4) -> (Vec3, Quat, Vec3) {
2744    let pos = Vec3::new(m.w_axis.x, m.w_axis.y, m.w_axis.z);
2745    let sx = Vec3::new(m.x_axis.x, m.x_axis.y, m.x_axis.z).length();
2746    let sy = Vec3::new(m.y_axis.x, m.y_axis.y, m.y_axis.z).length();
2747    let sz = Vec3::new(m.z_axis.x, m.z_axis.y, m.z_axis.z).length();
2748    let scale = Vec3::new(sx, sy, sz);
2749    let rot_mat = Mat4::from_cols(
2750        m.x_axis / sx,
2751        m.y_axis / sy,
2752        m.z_axis / sz,
2753        Vec4::W,
2754    );
2755    let rot = Quat::from_mat4(&rot_mat).normalize();
2756    (scale, rot, pos)
2757}
2758
2759// ─── Retargeting Pose Space ───────────────────────────────────────────────────
2760
2761/// Full clip-to-clip retargeting with pose-space correction.
2762pub struct ClipRetargeter {
2763    pub source_bind_pose: Vec<Transform>,
2764    pub target_bind_pose: Vec<Transform>,
2765    pub mappings: Vec<BoneMapping>,
2766}
2767
2768impl ClipRetargeter {
2769    pub fn new(
2770        source_bind: Vec<Transform>,
2771        target_bind: Vec<Transform>,
2772        mappings: Vec<BoneMapping>,
2773    ) -> Self {
2774        Self { source_bind_pose: source_bind, target_bind_pose: target_bind, mappings }
2775    }
2776
2777    pub fn retarget_keyframe(&self, source_tf: &Transform, mapping: &BoneMapping) -> Transform {
2778        let src_bone = mapping.source_bone as usize;
2779        let tgt_bone = mapping.target_bone as usize;
2780        if src_bone >= self.source_bind_pose.len() || tgt_bone >= self.target_bind_pose.len() {
2781            return *source_tf;
2782        }
2783        let src_bind = &self.source_bind_pose[src_bone];
2784        let tgt_bind = &self.target_bind_pose[tgt_bone];
2785
2786        // Compute local rotation delta from bind pose
2787        let local_rot = src_bind.rotation.inverse() * source_tf.rotation;
2788
2789        // Apply to target bind pose
2790        let new_rot = (tgt_bind.rotation * local_rot).normalize();
2791
2792        // Scale position by height ratio
2793        let new_pos = tgt_bind.position + (source_tf.position - src_bind.position) * mapping.scale_factor;
2794
2795        Transform { position: new_pos, rotation: new_rot, scale: source_tf.scale }
2796    }
2797
2798    pub fn retarget_clip(&self, source: &AnimationClip) -> AnimationClip {
2799        let mut result = AnimationClip::new(&source.name, source.frame_rate, source.duration);
2800        result.looping = source.looping;
2801        for track in &source.tracks {
2802            if let Some(mapping) = self.mappings.iter().find(|m| m.source_bone == track.bone_index) {
2803                let new_kfs: Vec<Keyframe> = track.keyframes.iter().map(|kf| {
2804                    Keyframe {
2805                        time: kf.time,
2806                        transform: self.retarget_keyframe(&kf.transform, mapping),
2807                    }
2808                }).collect();
2809                result.tracks.push(BoneTrack {
2810                    bone_index: mapping.target_bone,
2811                    keyframes: new_kfs,
2812                    importance: track.importance,
2813                });
2814            }
2815        }
2816        result
2817    }
2818}
2819
2820// ─── Animation Compression Stats Display ─────────────────────────────────────
2821
2822pub fn print_compression_report(report: &CompressionErrorReport) {
2823    let _ = format!(
2824        "=== Compression Report: {} ===\n\
2825         Max Position Error: {:.6} m\n\
2826         Max Rotation Error: {:.6} rad\n\
2827         RMS Position Error: {:.6} m\n\
2828         RMS Rotation Error: {:.6} rad\n\
2829         Keyframes: {} -> {} ({:.1}% reduction)\n\
2830         Bytes: {} -> {} ({:.2}x ratio)",
2831        report.clip_name,
2832        report.global_max_pos_error,
2833        report.global_max_rot_error,
2834        report.global_rms_pos_error,
2835        report.global_rms_rot_error,
2836        report.total_original_keyframes,
2837        report.total_compressed_keyframes,
2838        if report.total_original_keyframes > 0 {
2839            (1.0 - report.total_compressed_keyframes as f32 / report.total_original_keyframes as f32) * 100.0
2840        } else { 0.0 },
2841        report.byte_size_original,
2842        report.byte_size_compressed,
2843        if report.byte_size_compressed > 0 {
2844            report.byte_size_original as f32 / report.byte_size_compressed as f32
2845        } else { 0.0 },
2846    );
2847}
2848
2849// ─── Encode/Decode Full Compressed Clip to Bytes ──────────────────────────────
2850
2851pub fn encode_compressed_clip(clip: &CompressedAnimationClip) -> Vec<u8> {
2852    let mut data = Vec::new();
2853    // Header
2854    let name_bytes = clip.name.as_bytes();
2855    data.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
2856    data.extend_from_slice(name_bytes);
2857    data.extend_from_slice(&clip.frame_rate.to_bits().to_le_bytes());
2858    data.extend_from_slice(&clip.duration.to_bits().to_le_bytes());
2859    data.extend_from_slice(&(clip.looping as u8).to_le_bytes());
2860    data.extend_from_slice(&(clip.tracks.len() as u32).to_le_bytes());
2861    // Tracks
2862    for track in &clip.tracks {
2863        let track_bytes = serialize_compressed_track(track);
2864        data.extend_from_slice(&(track_bytes.len() as u32).to_le_bytes());
2865        data.extend_from_slice(&track_bytes);
2866    }
2867    data
2868}
2869
2870pub fn decode_compressed_clip(data: &[u8]) -> Option<CompressedAnimationClip> {
2871    if data.len() < 4 { return None; }
2872    let mut pos = 0usize;
2873
2874    let name_len = u32::from_le_bytes(data[pos..pos+4].try_into().ok()?) as usize;
2875    pos += 4;
2876    if pos + name_len > data.len() { return None; }
2877    let name = std::str::from_utf8(&data[pos..pos+name_len]).ok()?.to_owned();
2878    pos += name_len;
2879
2880    if pos + 9 > data.len() { return None; }
2881    let frame_rate = f32::from_bits(u32::from_le_bytes(data[pos..pos+4].try_into().ok()?));
2882    pos += 4;
2883    let duration = f32::from_bits(u32::from_le_bytes(data[pos..pos+4].try_into().ok()?));
2884    pos += 4;
2885    let looping = data[pos] != 0;
2886    pos += 1;
2887
2888    if pos + 4 > data.len() { return None; }
2889    let n_tracks = u32::from_le_bytes(data[pos..pos+4].try_into().ok()?) as usize;
2890    pos += 4;
2891
2892    let mut tracks = Vec::new();
2893    for _ in 0..n_tracks {
2894        if pos + 4 > data.len() { return None; }
2895        let track_len = u32::from_le_bytes(data[pos..pos+4].try_into().ok()?) as usize;
2896        pos += 4;
2897        if pos + track_len > data.len() { return None; }
2898        let track = deserialize_compressed_track(&data[pos..pos+track_len]);
2899        tracks.push(track);
2900        pos += track_len;
2901    }
2902
2903    let original_kf = tracks.iter().map(|t| t.keyframes.len()).sum();
2904    let compressed_bytes = tracks.iter().map(|t| t.byte_size()).sum();
2905
2906    Some(CompressedAnimationClip {
2907        name,
2908        frame_rate,
2909        duration,
2910        looping,
2911        tracks,
2912        original_keyframe_count: original_kf,
2913        compressed_keyframe_count: original_kf,
2914        original_byte_size: compressed_bytes,
2915        compressed_byte_size: compressed_bytes,
2916    })
2917}
2918
2919// ─── Sample Test Clip Builder ─────────────────────────────────────────────────
2920
2921/// Build a sample walking animation for testing.
2922pub fn build_sample_walk_clip(n_bones: u32, n_frames: usize, frame_rate: f32) -> AnimationClip {
2923    let duration = n_frames as f32 / frame_rate;
2924    let mut clip = AnimationClip::new("walk", frame_rate, duration);
2925    clip.looping = true;
2926
2927    for bone in 0..n_bones {
2928        let importance = if bone < 5 { 1.0 } else { 0.5 };
2929        let mut track = BoneTrack::new(bone, importance);
2930        for fi in 0..n_frames {
2931            let t = fi as f32 / frame_rate;
2932            let phase = t * std::f32::consts::TAU / duration;
2933            let pos = Vec3::new(
2934                (phase * (bone as f32 + 1.0)).sin() * 0.1,
2935                (phase * 2.0 + bone as f32).cos() * 0.05,
2936                0.0,
2937            );
2938            let rot = Quat::from_rotation_y((phase * 0.5 + bone as f32 * 0.1).sin() * 0.3);
2939            let scale = Vec3::ONE;
2940            track.push(Keyframe { time: t, transform: Transform { position: pos, rotation: rot, scale } });
2941        }
2942        clip.tracks.push(track);
2943    }
2944    clip
2945}
2946
2947// ─── Channel Mask ─────────────────────────────────────────────────────────────
2948
2949/// Per-channel compression mask: which channels to compress.
2950#[derive(Debug, Clone, Copy)]
2951pub struct ChannelMask {
2952    pub position: bool,
2953    pub rotation: bool,
2954    pub scale: bool,
2955}
2956
2957impl Default for ChannelMask {
2958    fn default() -> Self {
2959        Self { position: true, rotation: true, scale: true }
2960    }
2961}
2962
2963impl ChannelMask {
2964    pub fn rotation_only() -> Self {
2965        Self { position: false, rotation: true, scale: false }
2966    }
2967
2968    pub fn no_scale() -> Self {
2969        Self { position: true, rotation: true, scale: false }
2970    }
2971}
2972
2973/// Compress a track with per-channel masking.
2974pub fn compress_track_masked(
2975    track: &BoneTrack,
2976    settings: &CompressionSettings,
2977    mask: ChannelMask,
2978    tick_rate: f32,
2979) -> CompressedTrack {
2980    let bounds = PositionBounds::from_track(track);
2981    let keyframes = track.keyframes.iter().map(|kf| {
2982        let ticks = (kf.time * tick_rate * 1000.0) as u32;
2983        let position = if mask.position {
2984            quantize_position_16(kf.transform.position, &bounds)
2985        } else {
2986            [32767, 32767, 32767] // center
2987        };
2988        let rotation = if mask.rotation {
2989            compress_quat_smallest3(kf.transform.rotation)
2990        } else {
2991            CompressedQuat { largest_component: 0, components: [0, 0, 0] }
2992        };
2993        let scale = if mask.scale {
2994            quantize_scale_vec_log8(kf.transform.scale)
2995        } else {
2996            [128, 128, 128] // 1.0
2997        };
2998        CompressedKeyframe { time_ticks: ticks, position, rotation, scale }
2999    }).collect();
3000    CompressedTrack { bone_index: track.bone_index, pos_bounds: bounds, tick_rate, keyframes }
3001}
3002
3003// ─── Adaptive LOD Streaming ──────────────────────────────────────────────────
3004
3005pub struct LodStreamingManager {
3006    pub assets: HashMap<String, Vec<LodAnimationVariant>>,
3007    pub camera_distance_cache: HashMap<String, f32>,
3008}
3009
3010impl LodStreamingManager {
3011    pub fn new() -> Self {
3012        Self {
3013            assets: HashMap::new(),
3014            camera_distance_cache: HashMap::new(),
3015        }
3016    }
3017
3018    pub fn register_lod_variants(&mut self, name: &str, variants: Vec<LodAnimationVariant>) {
3019        self.assets.insert(name.to_owned(), variants);
3020    }
3021
3022    pub fn update_distance(&mut self, name: &str, distance: f32) {
3023        self.camera_distance_cache.insert(name.to_owned(), distance);
3024    }
3025
3026    pub fn get_active_variant(&self, name: &str) -> Option<&LodAnimationVariant> {
3027        let variants = self.assets.get(name)?;
3028        let &distance = self.camera_distance_cache.get(name).unwrap_or(&0.0);
3029        select_lod_variant(variants, distance)
3030    }
3031
3032    pub fn evaluate_pose(&self, name: &str, time: f32, n_bones: usize) -> Option<Vec<Transform>> {
3033        let variant = self.get_active_variant(name)?;
3034        let evaluator = PoseEvaluator::new(n_bones);
3035        Some(evaluator.evaluate(&variant.clip, time))
3036    }
3037}
3038
3039// ─── Compression Cache ────────────────────────────────────────────────────────
3040
3041pub struct CompressionCache {
3042    pub cache: HashMap<u64, CompressedAnimationClip>,
3043}
3044
3045impl CompressionCache {
3046    pub fn new() -> Self {
3047        Self { cache: HashMap::new() }
3048    }
3049
3050    pub fn key(clip_name: &str, settings: &CompressionSettings) -> u64 {
3051        // Simple hash combining name and settings
3052        let mut h = 0u64;
3053        for b in clip_name.bytes() {
3054            h = h.wrapping_mul(31).wrapping_add(b as u64);
3055        }
3056        h = h.wrapping_add((settings.pos_tolerance.to_bits() as u64) << 32);
3057        h = h.wrapping_add(settings.rot_tolerance_radians.to_bits() as u64);
3058        h
3059    }
3060
3061    pub fn get(&self, key: u64) -> Option<&CompressedAnimationClip> {
3062        self.cache.get(&key)
3063    }
3064
3065    pub fn insert(&mut self, key: u64, clip: CompressedAnimationClip) {
3066        self.cache.insert(key, clip);
3067    }
3068
3069    pub fn get_or_compress(
3070        &mut self,
3071        clip: &AnimationClip,
3072        settings: &CompressionSettings,
3073    ) -> CompressedAnimationClip {
3074        let key = Self::key(&clip.name, settings);
3075        if let Some(cached) = self.cache.get(&key) {
3076            return cached.clone();
3077        }
3078        let compressor = AnimationCompressor::new().with_settings(settings.clone());
3079        let compressed = compressor.compress(clip);
3080        self.cache.insert(key, compressed.clone());
3081        compressed
3082    }
3083}
3084
3085// ─── Keyframe Density Heatmap ─────────────────────────────────────────────────
3086
3087/// Compute a density histogram of keyframes across time.
3088pub fn keyframe_density_histogram(
3089    clip: &AnimationClip,
3090    n_buckets: usize,
3091) -> Vec<usize> {
3092    let mut counts = vec![0usize; n_buckets];
3093    if clip.duration < 1e-9 || n_buckets == 0 { return counts; }
3094    for track in &clip.tracks {
3095        for kf in &track.keyframes {
3096            let bucket = ((kf.time / clip.duration) * n_buckets as f32) as usize;
3097            let bucket = bucket.min(n_buckets - 1);
3098            counts[bucket] += 1;
3099        }
3100    }
3101    counts
3102}
3103
3104// ─── Compression Result Bundle ────────────────────────────────────────────────
3105
3106pub struct CompressionBundle {
3107    pub compressed: CompressedAnimationClip,
3108    pub error_report: CompressionErrorReport,
3109    pub lod_variants: Vec<CompressedAnimationClip>,
3110    pub streaming_asset: StreamingAnimationAsset,
3111}
3112
3113impl CompressionBundle {
3114    pub fn build(
3115        clip: &AnimationClip,
3116        settings: CompressionSettings,
3117        lod_distances: &[f32],
3118        bone_lod_info: Vec<BoneLodInfo>,
3119        chunk_size: usize,
3120    ) -> Self {
3121        let compressor = AnimationCompressor::new()
3122            .with_settings(settings)
3123            .with_bone_lod(bone_lod_info);
3124        let compressed = compressor.compress(clip);
3125        let error_report = compressor.error_analysis(clip, &compressed);
3126        let lod_variants = compressor.compress_with_lod(clip, lod_distances);
3127        let streaming_asset = compressor.build_streaming_asset(clip, chunk_size);
3128        Self { compressed, error_report, lod_variants, streaming_asset }
3129    }
3130
3131    pub fn summary(&self) -> String {
3132        format!(
3133            "Clip '{}': {:.2}x compression, {} LOD variants, {} chunks streamed",
3134            self.compressed.name,
3135            self.compressed.compression_ratio(),
3136            self.lod_variants.len(),
3137            self.streaming_asset.chunks.len(),
3138        )
3139    }
3140}
3141
3142// ─── Full Compression Test Harness ───────────────────────────────────────────
3143
3144pub fn run_compression_test() -> BatchCompressionStats {
3145    // Build test clips
3146    let clips: Vec<AnimationClip> = vec![
3147        build_sample_walk_clip(20, 120, 30.0),
3148        build_sample_walk_clip(10, 60, 24.0),
3149        build_sample_walk_clip(30, 240, 60.0),
3150    ];
3151
3152    let compressor = AnimationCompressor::new().with_settings(preset_medium_quality());
3153    let clip_refs: Vec<&AnimationClip> = clips.iter().collect();
3154    let compressed = compressor.batch_compress(&clip_refs);
3155
3156    BatchCompressionStats::compute(&clip_refs, &compressed)
3157}
3158
3159// ─── Skinning Weight Compression ─────────────────────────────────────────────
3160
3161#[derive(Debug, Clone)]
3162pub struct SkinWeight {
3163    pub bone_index: u8,
3164    pub weight: f32,
3165}
3166
3167#[derive(Debug, Clone)]
3168pub struct SkinWeightSet {
3169    pub weights: Vec<SkinWeight>,
3170}
3171
3172impl SkinWeightSet {
3173    pub fn new() -> Self {
3174        Self { weights: Vec::new() }
3175    }
3176
3177    pub fn normalize(&mut self) {
3178        let total: f32 = self.weights.iter().map(|w| w.weight).sum();
3179        if total > 1e-9 {
3180            for w in &mut self.weights {
3181                w.weight /= total;
3182            }
3183        }
3184    }
3185
3186    /// Quantize weights to 8-bit integers (sum must = 255 after quantization).
3187    pub fn quantize_u8(&self) -> Vec<(u8, u8)> {
3188        let mut quantized: Vec<(u8, u8)> = self.weights.iter().map(|w| {
3189            (w.bone_index, (w.weight * 255.0).round() as u8)
3190        }).collect();
3191        // Fix rounding error
3192        let sum: u32 = quantized.iter().map(|(_, w)| *w as u32).sum();
3193        if sum > 0 && sum != 255 {
3194            // Adjust the largest weight
3195            if let Some(max_idx) = quantized.iter().enumerate().max_by_key(|(_, (_, w))| *w).map(|(i, _)| i) {
3196                let diff = 255i32 - sum as i32;
3197                quantized[max_idx].1 = (quantized[max_idx].1 as i32 + diff).max(0).min(255) as u8;
3198            }
3199        }
3200        quantized
3201    }
3202}
3203
3204// ─── Animation Compression State Machine ─────────────────────────────────────
3205
3206#[derive(Debug, Clone, PartialEq)]
3207pub enum CompressionJobStatus {
3208    Pending,
3209    Running,
3210    Complete,
3211    Failed(String),
3212}
3213
3214#[derive(Debug, Clone)]
3215pub struct CompressionJob {
3216    pub id: u64,
3217    pub clip_name: String,
3218    pub settings: CompressionSettings,
3219    pub status: CompressionJobStatus,
3220    pub result: Option<CompressedAnimationClip>,
3221}
3222
3223impl CompressionJob {
3224    pub fn new(id: u64, clip_name: &str, settings: CompressionSettings) -> Self {
3225        Self {
3226            id,
3227            clip_name: clip_name.to_owned(),
3228            settings,
3229            status: CompressionJobStatus::Pending,
3230            result: None,
3231        }
3232    }
3233}
3234
3235pub struct CompressionJobQueue {
3236    pub pending: VecDeque<CompressionJob>,
3237    pub running: Option<CompressionJob>,
3238    pub completed: Vec<CompressionJob>,
3239    pub registry: AnimationRegistry,
3240}
3241
3242impl CompressionJobQueue {
3243    pub fn new() -> Self {
3244        Self {
3245            pending: VecDeque::new(),
3246            running: None,
3247            completed: Vec::new(),
3248            registry: AnimationRegistry::new(),
3249        }
3250    }
3251
3252    pub fn enqueue(&mut self, job: CompressionJob) {
3253        self.pending.push_back(job);
3254    }
3255
3256    pub fn tick(&mut self) {
3257        if self.running.is_some() { return; }
3258        if let Some(mut job) = self.pending.pop_front() {
3259            job.status = CompressionJobStatus::Running;
3260            if let Some(clip) = self.registry.clips.get(&job.clip_name) {
3261                let compressor = AnimationCompressor::new().with_settings(job.settings.clone());
3262                let compressed = compressor.compress(clip);
3263                job.result = Some(compressed);
3264                job.status = CompressionJobStatus::Complete;
3265            } else {
3266                job.status = CompressionJobStatus::Failed(format!("Clip '{}' not found", job.clip_name));
3267            }
3268            self.completed.push(job);
3269        }
3270    }
3271
3272    pub fn results(&self) -> impl Iterator<Item = &CompressedAnimationClip> {
3273        self.completed.iter().filter_map(|j| j.result.as_ref())
3274    }
3275}
3276
3277// ─── Extra: Piecewise Linear Approximation ────────────────────────────────────
3278
3279pub struct PiecewiseLinearCurve {
3280    pub times: Vec<f32>,
3281    pub values: Vec<f32>,
3282}
3283
3284impl PiecewiseLinearCurve {
3285    pub fn new(times: Vec<f32>, values: Vec<f32>) -> Self {
3286        Self { times, values }
3287    }
3288
3289    pub fn sample(&self, t: f32) -> f32 {
3290        if self.times.is_empty() { return 0.0; }
3291        if self.times.len() == 1 { return self.values[0]; }
3292        let idx = self.times.partition_point(|&ti| ti <= t);
3293        if idx == 0 { return self.values[0]; }
3294        if idx >= self.times.len() { return *self.values.last().unwrap(); }
3295        let t0 = self.times[idx - 1];
3296        let t1 = self.times[idx];
3297        let v0 = self.values[idx - 1];
3298        let v1 = self.values[idx];
3299        let dt = t1 - t0;
3300        let alpha = if dt > 1e-9 { (t - t0) / dt } else { 0.0 };
3301        v0 + (v1 - v0) * alpha
3302    }
3303
3304    pub fn reduce_rdp(&self, eps: f32) -> Self {
3305        if self.times.len() <= 2 {
3306            return Self::new(self.times.clone(), self.values.clone());
3307        }
3308        let kfs: Vec<ScalarKeyframe> = self.times.iter().zip(self.values.iter())
3309            .map(|(&t, &v)| ScalarKeyframe { time: t, value: v })
3310            .collect();
3311        let curve = ScalarCurve { channel: CurveChannel::PosX, keyframes: kfs };
3312        let reduced = curve.rdp_reduce(eps);
3313        Self::new(
3314            reduced.keyframes.iter().map(|k| k.time).collect(),
3315            reduced.keyframes.iter().map(|k| k.value).collect(),
3316        )
3317    }
3318}
3319
3320// ─── Bone Hierarchy Compression ───────────────────────────────────────────────
3321
3322#[derive(Debug, Clone)]
3323pub struct BoneHierarchy {
3324    pub n_bones: usize,
3325    pub parent_indices: Vec<Option<u32>>,
3326    pub bone_names: Vec<String>,
3327}
3328
3329impl BoneHierarchy {
3330    pub fn new(n_bones: usize) -> Self {
3331        Self {
3332            n_bones,
3333            parent_indices: vec![None; n_bones],
3334            bone_names: (0..n_bones).map(|i| format!("bone_{}", i)).collect(),
3335        }
3336    }
3337
3338    pub fn set_parent(&mut self, bone: u32, parent: u32) {
3339        if (bone as usize) < self.n_bones {
3340            self.parent_indices[bone as usize] = Some(parent);
3341        }
3342    }
3343
3344    pub fn root_bones(&self) -> Vec<u32> {
3345        self.parent_indices.iter().enumerate()
3346            .filter(|(_, p)| p.is_none())
3347            .map(|(i, _)| i as u32)
3348            .collect()
3349    }
3350
3351    pub fn children_of(&self, bone: u32) -> Vec<u32> {
3352        self.parent_indices.iter().enumerate()
3353            .filter(|(_, &p)| p == Some(bone))
3354            .map(|(i, _)| i as u32)
3355            .collect()
3356    }
3357
3358    pub fn depth_of(&self, bone: u32) -> u32 {
3359        let mut depth = 0;
3360        let mut current = bone as usize;
3361        for _ in 0..self.n_bones {
3362            match self.parent_indices[current] {
3363                Some(p) => { depth += 1; current = p as usize; }
3364                None => break,
3365            }
3366        }
3367        depth
3368    }
3369}
3370
3371// ─── Bone importance from hierarchy ──────────────────────────────────────────
3372
3373pub fn compute_hierarchy_importance(hierarchy: &BoneHierarchy) -> Vec<f32> {
3374    let mut importance = vec![0.5f32; hierarchy.n_bones];
3375    for i in 0..hierarchy.n_bones {
3376        let depth = hierarchy.depth_of(i as u32);
3377        // Root bones are more important; leaves less
3378        let n_children = hierarchy.children_of(i as u32).len();
3379        let child_factor = if n_children == 0 { 0.5 } else { 1.0 };
3380        let depth_factor = (1.0 / (1.0 + depth as f32 * 0.1)).max(0.1);
3381        importance[i] = (depth_factor * child_factor).min(1.0);
3382    }
3383    importance
3384}
3385
3386// ─── Final Utilities ──────────────────────────────────────────────────────────
3387
3388/// Compute clip bounding box (over all bone positions, all frames).
3389pub fn clip_bounding_box(clip: &AnimationClip) -> (Vec3, Vec3) {
3390    let mut min = Vec3::splat(f32::MAX);
3391    let mut max = Vec3::splat(f32::MIN);
3392    for track in &clip.tracks {
3393        for kf in &track.keyframes {
3394            min = min.min(kf.transform.position);
3395            max = max.max(kf.transform.position);
3396        }
3397    }
3398    (min, max)
3399}
3400
3401/// Check if a clip has any non-identity motion.
3402pub fn clip_has_motion(clip: &AnimationClip, eps: f32) -> bool {
3403    for track in &clip.tracks {
3404        for kf in &track.keyframes {
3405            if position_error_l2(kf.transform.position, Vec3::ZERO) > eps { return true; }
3406            if rotation_error_geodesic(kf.transform.rotation, Quat::IDENTITY) > eps { return true; }
3407        }
3408    }
3409    false
3410}
3411
3412/// Scale all positions in a clip by a uniform factor.
3413pub fn scale_clip_positions(clip: &mut AnimationClip, scale: f32) {
3414    for track in &mut clip.tracks {
3415        for kf in &mut track.keyframes {
3416            kf.transform.position *= scale;
3417        }
3418    }
3419}
3420
3421/// Time-scale a clip (compress/expand time axis).
3422pub fn time_scale_clip(clip: &mut AnimationClip, time_scale: f32) {
3423    if time_scale.abs() < 1e-9 { return; }
3424    clip.duration /= time_scale;
3425    clip.frame_rate *= time_scale;
3426    for track in &mut clip.tracks {
3427        for kf in &mut track.keyframes {
3428            kf.time /= time_scale;
3429        }
3430    }
3431}
3432
3433/// Reverse an animation clip.
3434pub fn reverse_clip(clip: &AnimationClip) -> AnimationClip {
3435    let mut reversed = clip.clone();
3436    reversed.name = format!("{}_reversed", clip.name);
3437    for track in &mut reversed.tracks {
3438        track.keyframes.reverse();
3439        for kf in &mut track.keyframes {
3440            kf.time = clip.duration - kf.time;
3441        }
3442        track.keyframes.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
3443    }
3444    reversed
3445}
3446
3447/// Bake additive layer into the base animation.
3448pub fn bake_additive_into_base(
3449    base: &AnimationClip,
3450    additive: &AnimationClip,
3451    weight: f32,
3452) -> AnimationClip {
3453    let mut result = base.clone();
3454    result.name = format!("{}_baked", base.name);
3455    let evaluator = PoseEvaluator::new(MAX_BONES);
3456    for track in &mut result.tracks {
3457        for kf in &mut track.keyframes {
3458            if let Some(add_t) = additive.sample_at(track.bone_index, kf.time) {
3459                kf.transform = apply_additive_layer(&kf.transform, &add_t, weight);
3460            }
3461        }
3462    }
3463    result
3464}
3465
3466// ─── Compression Round-trip Validation ───────────────────────────────────────
3467
3468pub fn validate_round_trip(
3469    clip: &AnimationClip,
3470    compressor: &AnimationCompressor,
3471    max_acceptable_pos_error: f32,
3472    max_acceptable_rot_error: f32,
3473) -> Result<CompressionErrorReport, String> {
3474    let compressed = compressor.compress(clip);
3475    let report = compute_error_report(clip, &compressed);
3476    if report.global_max_pos_error > max_acceptable_pos_error {
3477        return Err(format!(
3478            "Position error {:.6} exceeds threshold {:.6}",
3479            report.global_max_pos_error, max_acceptable_pos_error
3480        ));
3481    }
3482    if report.global_max_rot_error > max_acceptable_rot_error {
3483        return Err(format!(
3484            "Rotation error {:.6} exceeds threshold {:.6}",
3485            report.global_max_rot_error, max_acceptable_rot_error
3486        ));
3487    }
3488    Ok(report)
3489}
3490
3491// ─── Export Manifest ──────────────────────────────────────────────────────────
3492
3493#[derive(Debug, Clone)]
3494pub struct AnimationExportManifest {
3495    pub clips: Vec<String>,
3496    pub total_compressed_bytes: usize,
3497    pub total_original_bytes: usize,
3498    pub export_time_ms: u64,
3499    pub settings: CompressionSettings,
3500}
3501
3502impl AnimationExportManifest {
3503    pub fn build(
3504        clips: &[&AnimationClip],
3505        compressed: &[CompressedAnimationClip],
3506        settings: CompressionSettings,
3507        export_time_ms: u64,
3508    ) -> Self {
3509        Self {
3510            clips: clips.iter().map(|c| c.name.clone()).collect(),
3511            total_original_bytes: clips.iter().map(|c| c.total_keyframes() * 40).sum(),
3512            total_compressed_bytes: compressed.iter().map(|c| c.compressed_byte_size).sum(),
3513            export_time_ms,
3514            settings,
3515        }
3516    }
3517
3518    pub fn compression_ratio(&self) -> f32 {
3519        if self.total_compressed_bytes == 0 { 0.0 }
3520        else { self.total_original_bytes as f32 / self.total_compressed_bytes as f32 }
3521    }
3522}
3523
3524// ─── Hermite Rotation Spline ─────────────────────────────────────────────────
3525
3526/// Cubic Hermite spline for rotations (in quaternion log-space).
3527pub struct RotationHermiteSpline {
3528    pub times: Vec<f32>,
3529    pub rotations: Vec<Quat>,
3530    pub tangents: Vec<Vec3>, // log-space tangents
3531}
3532
3533impl RotationHermiteSpline {
3534    pub fn from_keyframes(kfs: &[Keyframe]) -> Self {
3535        let times: Vec<f32> = kfs.iter().map(|k| k.time).collect();
3536        let rotations: Vec<Quat> = kfs.iter().map(|k| k.transform.rotation).collect();
3537        let n = times.len();
3538        let mut tangents = vec![Vec3::ZERO; n];
3539
3540        for i in 0..n {
3541            if i == 0 || i == n - 1 { continue; }
3542            let dt_p = times[i+1] - times[i];
3543            let dt_m = times[i] - times[i-1];
3544            let dt = times[i+1] - times[i-1];
3545            if dt < 1e-9 { continue; }
3546            // Log-space derivative
3547            let log_p = quat_log(rotations[i].inverse() * rotations[i+1]);
3548            let log_m = quat_log(rotations[i-1].inverse() * rotations[i]);
3549            tangents[i] = (log_m / dt_m + log_p / dt_p) * 0.5;
3550        }
3551
3552        Self { times, rotations, tangents }
3553    }
3554
3555    pub fn sample(&self, t: f32) -> Quat {
3556        if self.times.is_empty() { return Quat::IDENTITY; }
3557        let idx = self.times.partition_point(|&ti| ti <= t);
3558        if idx == 0 { return self.rotations[0]; }
3559        if idx >= self.times.len() { return *self.rotations.last().unwrap(); }
3560        let t0 = self.times[idx-1];
3561        let t1 = self.times[idx];
3562        let dt = t1 - t0;
3563        let s = if dt > 1e-9 { (t - t0) / dt } else { 0.0 };
3564        let q0 = self.rotations[idx-1];
3565        let q1 = self.rotations[idx];
3566        q0.slerp(q1, s)
3567    }
3568}
3569
3570fn quat_log(q: Quat) -> Vec3 {
3571    let len = Vec3::new(q.x, q.y, q.z).length();
3572    if len < 1e-9 { return Vec3::ZERO; }
3573    let angle = 2.0 * len.atan2(q.w);
3574    Vec3::new(q.x, q.y, q.z) * (angle / len)
3575}
3576
3577fn quat_exp(v: Vec3) -> Quat {
3578    let angle = v.length();
3579    if angle < 1e-9 { return Quat::IDENTITY; }
3580    let axis = v / angle;
3581    Quat::from_axis_angle(axis, angle)
3582}
3583
3584// ─── Pose Blending: Multi-source ─────────────────────────────────────────────
3585
3586pub fn blend_n_poses(poses: &[Vec<Transform>], weights: &[f32]) -> Vec<Transform> {
3587    assert!(!poses.is_empty());
3588    let n_bones = poses[0].len();
3589    let total_w: f32 = weights.iter().sum();
3590    if total_w < 1e-9 {
3591        return vec![Transform::identity(); n_bones];
3592    }
3593
3594    let mut result = vec![Transform::identity(); n_bones];
3595    let mut acc_w = 0.0f32;
3596
3597    for (pose, &w) in poses.iter().zip(weights.iter()) {
3598        let norm_w = w / total_w;
3599        let t = norm_w / (acc_w + norm_w).max(1e-9);
3600        for (r, p) in result.iter_mut().zip(pose.iter()) {
3601            *r = r.lerp(p, t);
3602        }
3603        acc_w += norm_w;
3604    }
3605    result
3606}
3607
3608// ─── Quantization Error Table ────────────────────────────────────────────────
3609
3610/// Compute the worst-case quantization error for a given number of bits and range.
3611pub fn quantization_error_worst_case(range: f32, bits: u32) -> f32 {
3612    let n_steps = (1u64 << bits) as f32;
3613    range / n_steps / 2.0
3614}
3615
3616pub fn quantization_error_rotation_16bit() -> f32 {
3617    // Smallest-3 method: each component has range 1/sqrt(2), stored as i16
3618    let range = 2.0 * SMALL3_SCALE;
3619    quantization_error_worst_case(range, 15) // 15 bits for magnitude (1 for sign)
3620}
3621
3622pub fn quantization_error_position_16bit(range: f32) -> f32 {
3623    quantization_error_worst_case(range, 16)
3624}
3625
3626pub fn quantization_error_scale_8bit() -> f32 {
3627    // Log scale: range [log2(1/16), log2(16)] = [-4, 4]
3628    quantization_error_worst_case(8.0, 8)
3629}
3630
3631// ─── Integration with Bone Rigger ─────────────────────────────────────────────
3632
3633#[derive(Debug, Clone)]
3634pub struct RiggedAnimationSet {
3635    pub skeleton: BoneHierarchy,
3636    pub bind_pose: Vec<Transform>,
3637    pub clips: Vec<AnimationClip>,
3638    pub compressed_clips: Vec<CompressedAnimationClip>,
3639    pub lod_info: Vec<BoneLodInfo>,
3640}
3641
3642impl RiggedAnimationSet {
3643    pub fn new(skeleton: BoneHierarchy, bind_pose: Vec<Transform>) -> Self {
3644        Self {
3645            skeleton,
3646            bind_pose,
3647            clips: Vec::new(),
3648            compressed_clips: Vec::new(),
3649            lod_info: Vec::new(),
3650        }
3651    }
3652
3653    pub fn add_clip(&mut self, clip: AnimationClip) {
3654        self.clips.push(clip);
3655    }
3656
3657    pub fn compress_all(&mut self, settings: CompressionSettings) {
3658        let compressor = AnimationCompressor::new()
3659            .with_settings(settings)
3660            .with_bone_lod(self.lod_info.clone());
3661        let clip_refs: Vec<&AnimationClip> = self.clips.iter().collect();
3662        self.compressed_clips = compressor.batch_compress(&clip_refs);
3663    }
3664
3665    pub fn evaluate_pose(&self, clip_name: &str, time: f32) -> Vec<Transform> {
3666        // Try compressed first
3667        let clip = self.compressed_clips.iter().find(|c| c.name == clip_name);
3668        if let Some(c) = clip {
3669            let evaluator = PoseEvaluator::new(self.skeleton.n_bones);
3670            return evaluator.evaluate_compressed(c, time);
3671        }
3672        // Fall back to raw
3673        if let Some(c) = self.clips.iter().find(|c| c.name == clip_name) {
3674            let evaluator = PoseEvaluator::new(self.skeleton.n_bones);
3675            return evaluator.evaluate(c, time);
3676        }
3677        vec![Transform::identity(); self.skeleton.n_bones]
3678    }
3679}
3680
3681// ─── Extended: Bone Constraint System ────────────────────────────────────────
3682
3683#[derive(Debug, Clone, Copy, PartialEq)]
3684pub enum ConstraintType {
3685    LookAt, Aim, OrientLike, CopyLocation,
3686    LimitRotation, LimitLocation, LimitScale,
3687    StretchTo, TrackTo, ClampTo, SplineIK,
3688}
3689
3690#[derive(Debug, Clone)]
3691pub struct BoneConstraint {
3692    pub bone_index: u32,
3693    pub constraint_type: ConstraintType,
3694    pub target_bone: Option<u32>,
3695    pub influence: f32,
3696    pub min: Vec3,
3697    pub max: Vec3,
3698    pub enabled: bool,
3699}
3700
3701impl BoneConstraint {
3702    pub fn look_at(bone: u32, target: u32) -> Self {
3703        Self { bone_index: bone, constraint_type: ConstraintType::LookAt, target_bone: Some(target), influence: 1.0, min: Vec3::NEG_ONE, max: Vec3::ONE, enabled: true }
3704    }
3705
3706    pub fn limit_rotation(bone: u32, min: Vec3, max: Vec3) -> Self {
3707        Self { bone_index: bone, constraint_type: ConstraintType::LimitRotation, target_bone: None, influence: 1.0, min, max, enabled: true }
3708    }
3709
3710    pub fn apply_look_at(bone_transform: &Transform, target_world_pos: Vec3, up: Vec3) -> Transform {
3711        let dir = (target_world_pos - bone_transform.position).normalize();
3712        let right = up.cross(dir).normalize();
3713        let up_correct = dir.cross(right);
3714        let rot = Quat::from_mat4(&Mat4::from_cols(
3715            Vec4::new(right.x, right.y, right.z, 0.0),
3716            Vec4::new(up_correct.x, up_correct.y, up_correct.z, 0.0),
3717            Vec4::new(dir.x, dir.y, dir.z, 0.0),
3718            Vec4::W,
3719        )).normalize();
3720        Transform { rotation: rot, ..*bone_transform }
3721    }
3722
3723    pub fn apply_limit_rotation(t: &Transform, min: Vec3, max: Vec3) -> Transform {
3724        let (yaw, pitch, roll) = quat_to_euler_yxz(t.rotation);
3725        let clamped_rot = euler_yxz_to_quat(yaw.clamp(min.y, max.y), pitch.clamp(min.x, max.x), roll.clamp(min.z, max.z));
3726        Transform { rotation: clamped_rot, ..*t }
3727    }
3728}
3729
3730fn quat_to_euler_yxz(q: Quat) -> (f32, f32, f32) {
3731    let sinr_cosp = 2.0 * (q.w * q.x + q.y * q.z);
3732    let cosr_cosp = 1.0 - 2.0 * (q.x * q.x + q.y * q.y);
3733    let roll = sinr_cosp.atan2(cosr_cosp);
3734    let sinp = 2.0 * (q.w * q.y - q.z * q.x);
3735    let pitch = if sinp.abs() >= 1.0 { sinp.signum() * std::f32::consts::FRAC_PI_2 } else { sinp.asin() };
3736    let siny_cosp = 2.0 * (q.w * q.z + q.x * q.y);
3737    let cosy_cosp = 1.0 - 2.0 * (q.y * q.y + q.z * q.z);
3738    let yaw = siny_cosp.atan2(cosy_cosp);
3739    (yaw, pitch, roll)
3740}
3741
3742fn euler_yxz_to_quat(yaw: f32, pitch: f32, roll: f32) -> Quat {
3743    let cy = (yaw * 0.5).cos(); let sy = (yaw * 0.5).sin();
3744    let cp = (pitch * 0.5).cos(); let sp = (pitch * 0.5).sin();
3745    let cr = (roll * 0.5).cos(); let sr = (roll * 0.5).sin();
3746    Quat::from_xyzw(
3747        cy * sp * cr + sy * cp * sr,
3748        sy * cp * cr - cy * sp * sr,
3749        cy * cp * sr - sy * sp * cr,
3750        cy * cp * cr + sy * sp * sr,
3751    ).normalize()
3752}
3753
3754// ─── Extended: Animation State Machine Blending ───────────────────────────────
3755
3756#[derive(Debug, Clone, PartialEq)]
3757pub enum BlendStrategy { Linear, Cubic, Additive, Override }
3758
3759#[derive(Debug, Clone)]
3760pub struct AnimationStateEntry {
3761    pub name: String,
3762    pub clip_name: String,
3763    pub speed: f32,
3764    pub looping: bool,
3765    pub blend_in_time: f32,
3766    pub blend_out_time: f32,
3767}
3768
3769impl AnimationStateEntry {
3770    pub fn new(name: &str, clip_name: &str) -> Self {
3771        Self { name: name.to_owned(), clip_name: clip_name.to_owned(), speed: 1.0, looping: false, blend_in_time: 0.2, blend_out_time: 0.2 }
3772    }
3773}
3774
3775#[derive(Debug, Clone)]
3776pub struct StateTransition {
3777    pub from: String, pub to: String,
3778    pub duration: f32, pub blend_strategy: BlendStrategy,
3779}
3780
3781pub struct LayeredAnimationStateMachine {
3782    pub states: HashMap<String, AnimationStateEntry>,
3783    pub transitions: Vec<StateTransition>,
3784    pub current_state: Option<String>,
3785    pub next_state: Option<String>,
3786    pub blend_alpha: f32,
3787    pub elapsed: f32,
3788}
3789
3790impl LayeredAnimationStateMachine {
3791    pub fn new() -> Self {
3792        Self { states: HashMap::new(), transitions: Vec::new(), current_state: None, next_state: None, blend_alpha: 0.0, elapsed: 0.0 }
3793    }
3794
3795    pub fn add_state(&mut self, s: AnimationStateEntry) { self.states.insert(s.name.clone(), s); }
3796    pub fn add_transition(&mut self, t: StateTransition) { self.transitions.push(t); }
3797
3798    pub fn trigger(&mut self, name: &str) {
3799        if self.states.contains_key(name) { self.next_state = Some(name.to_owned()); self.blend_alpha = 0.0; }
3800    }
3801
3802    pub fn update(&mut self, dt: f32) -> f32 {
3803        self.elapsed += dt;
3804        if let Some(ref next) = self.next_state.clone() {
3805            let dur = self.transitions.iter()
3806                .find(|t| self.current_state.as_deref() == Some(&t.from) && t.to == *next)
3807                .map_or(0.2, |t| t.duration);
3808            self.blend_alpha = (self.blend_alpha + dt / dur.max(0.001)).min(1.0);
3809            if self.blend_alpha >= 1.0 {
3810                self.current_state = Some(next.clone());
3811                self.next_state = None;
3812                self.blend_alpha = 0.0;
3813            }
3814        }
3815        self.blend_alpha
3816    }
3817
3818    pub fn evaluate_pose(&self, registry: &AnimationRegistry, time: f32, n_bones: usize) -> Vec<Transform> {
3819        let evaluator = PoseEvaluator::new(n_bones);
3820        let eval_state = |name: &str| -> Vec<Transform> {
3821            if let Some(state) = self.states.get(name) {
3822                if let Some(clip) = registry.get_compressed(&state.clip_name) {
3823                    return evaluator.evaluate_compressed(clip, time * state.speed);
3824                }
3825                if let Some(clip) = registry.get_clip(&state.clip_name) {
3826                    return evaluator.evaluate(clip, time * state.speed);
3827                }
3828            }
3829            vec![Transform::identity(); n_bones]
3830        };
3831        let cur = self.current_state.as_deref().unwrap_or("");
3832        let nxt = self.next_state.as_deref().unwrap_or(cur);
3833        let pose_a = eval_state(cur);
3834        if self.blend_alpha < 1e-6 || cur == nxt { return pose_a; }
3835        let pose_b = eval_state(nxt);
3836        evaluator.blend_poses(&pose_a, &pose_b, self.blend_alpha)
3837    }
3838}
3839
3840// ─── Extended: Animation Frame Cache ─────────────────────────────────────────
3841
3842pub struct AnimationFrameCache {
3843    pub cache: HashMap<(String, u32), Vec<Transform>>,
3844    pub max_entries: usize,
3845    pub access_order: VecDeque<(String, u32)>,
3846}
3847
3848impl AnimationFrameCache {
3849    pub fn new(max_entries: usize) -> Self {
3850        Self { cache: HashMap::new(), max_entries, access_order: VecDeque::new() }
3851    }
3852
3853    pub fn get_or_compute(&mut self, clip: &AnimationClip, frame: u32, n_bones: usize) -> Vec<Transform> {
3854        let key = (clip.name.clone(), frame);
3855        if let Some(pose) = self.cache.get(&key) {
3856            self.access_order.retain(|k| k != &key);
3857            self.access_order.push_back(key);
3858            return pose.clone();
3859        }
3860        let t = frame as f32 / clip.frame_rate;
3861        let evaluator = PoseEvaluator::new(n_bones);
3862        let pose = evaluator.evaluate(clip, t);
3863        if self.cache.len() >= self.max_entries {
3864            if let Some(oldest) = self.access_order.pop_front() { self.cache.remove(&oldest); }
3865        }
3866        self.cache.insert(key.clone(), pose.clone());
3867        self.access_order.push_back(key);
3868        pose
3869    }
3870
3871    pub fn invalidate(&mut self, clip_name: &str) {
3872        self.cache.retain(|(name, _), _| name != clip_name);
3873        self.access_order.retain(|(name, _)| name != clip_name);
3874    }
3875
3876    pub fn memory_usage_estimate(&self) -> usize {
3877        self.cache.values().map(|p| p.len() * std::mem::size_of::<Transform>()).sum()
3878    }
3879}
3880
3881// ─── Extended: Pose Utilities ─────────────────────────────────────────────────
3882
3883pub fn pose_diff(pose_a: &[Transform], pose_b: &[Transform]) -> Vec<TransformDelta> {
3884    pose_a.iter().zip(pose_b.iter()).map(|(a, b)| TransformDelta::compute(a, b)).collect()
3885}
3886
3887pub fn poses_equal(pose_a: &[Transform], pose_b: &[Transform], eps: f32) -> bool {
3888    if pose_a.len() != pose_b.len() { return false; }
3889    pose_a.iter().zip(pose_b.iter()).all(|(a, b)| {
3890        position_error_l2(a.position, b.position) < eps
3891            && rotation_error_geodesic(a.rotation, b.rotation) < eps
3892            && scale_error_l2(a.scale, b.scale) < eps
3893    })
3894}
3895
3896pub fn extrapolate_pose(pose: &[Transform], prev_pose: &[Transform], dt: f32, frame_dt: f32) -> Vec<Transform> {
3897    let factor = if frame_dt > 1e-9 { dt / frame_dt } else { 0.0 };
3898    pose.iter().zip(prev_pose.iter()).map(|(cur, prev)| {
3899        let pos_vel = cur.position - prev.position;
3900        let rot_vel = prev.rotation.inverse() * cur.rotation;
3901        let extra_rot = Quat::IDENTITY.slerp(rot_vel, factor);
3902        Transform {
3903            position: cur.position + pos_vel * factor,
3904            rotation: (cur.rotation * extra_rot).normalize(),
3905            scale: cur.scale,
3906        }
3907    }).collect()
3908}
3909
3910// ─── Extended: Motion Matching Database ──────────────────────────────────────
3911
3912#[derive(Debug, Clone)]
3913pub struct MotionPose {
3914    pub positions: Vec<Vec3>,
3915    pub velocities: Vec<Vec3>,
3916    pub trajectory: Vec<Vec3>,
3917    pub clip_name: String,
3918    pub frame: u32,
3919}
3920
3921impl MotionPose {
3922    pub fn distance(&self, other: &MotionPose, pw: f32, vw: f32, tw: f32) -> f32 {
3923        let pe: f32 = self.positions.iter().zip(other.positions.iter()).map(|(a,b)| (*a - *b).length_squared()).sum::<f32>().sqrt();
3924        let ve: f32 = self.velocities.iter().zip(other.velocities.iter()).map(|(a,b)| (*a - *b).length_squared()).sum::<f32>().sqrt();
3925        let te: f32 = self.trajectory.iter().zip(other.trajectory.iter()).map(|(a,b)| (*a - *b).length_squared()).sum::<f32>().sqrt();
3926        pe * pw + ve * vw + te * tw
3927    }
3928}
3929
3930pub struct MotionMatchingDb {
3931    pub poses: Vec<MotionPose>,
3932}
3933
3934impl MotionMatchingDb {
3935    pub fn new() -> Self { Self { poses: Vec::new() } }
3936
3937    pub fn add(&mut self, p: MotionPose) { self.poses.push(p); }
3938
3939    pub fn find_best(&self, query: &MotionPose, pw: f32, vw: f32, tw: f32) -> Option<&MotionPose> {
3940        self.poses.iter().min_by(|a, b| {
3941            a.distance(query, pw, vw, tw).partial_cmp(&b.distance(query, pw, vw, tw)).unwrap_or(std::cmp::Ordering::Equal)
3942        })
3943    }
3944
3945    pub fn build_from_clip(clip: &AnimationClip, n_bones: usize, traj_steps: usize, traj_dt: f32) -> Self {
3946        let mut db = Self::new();
3947        let frames = (clip.duration * clip.frame_rate) as u32;
3948        let ev = PoseEvaluator::new(n_bones);
3949        for frame in 0..frames {
3950            let t = frame as f32 / clip.frame_rate;
3951            let pose = ev.evaluate(clip, t);
3952            let pose_prev = ev.evaluate(clip, (t - 1.0 / clip.frame_rate).max(0.0));
3953            let positions: Vec<Vec3> = pose.iter().map(|b| b.position).collect();
3954            let velocities: Vec<Vec3> = pose.iter().zip(pose_prev.iter()).map(|(c,p)| c.position - p.position).collect();
3955            let trajectory: Vec<Vec3> = (1..=traj_steps).map(|si| {
3956                let ft = (t + si as f32 * traj_dt).min(clip.duration);
3957                ev.evaluate(clip, ft).get(0).map_or(Vec3::ZERO, |b| b.position)
3958            }).collect();
3959            db.add(MotionPose { positions, velocities, trajectory, clip_name: clip.name.clone(), frame });
3960        }
3961        db
3962    }
3963}
3964
3965// ─── Extended: Foot IK ───────────────────────────────────────────────────────
3966
3967#[derive(Debug, Clone)]
3968pub struct FootIKSolver {
3969    pub left_foot_bone: u32,
3970    pub right_foot_bone: u32,
3971    pub left_plant_threshold: f32,
3972    pub right_plant_threshold: f32,
3973    pub ik_blend: f32,
3974    pub left_planted: bool,
3975    pub right_planted: bool,
3976    pub left_plant_pos: Vec3,
3977    pub right_plant_pos: Vec3,
3978}
3979
3980impl FootIKSolver {
3981    pub fn new(left: u32, right: u32) -> Self {
3982        Self { left_foot_bone: left, right_foot_bone: right, left_plant_threshold: 0.05, right_plant_threshold: 0.05, ik_blend: 1.0, left_planted: false, right_planted: false, left_plant_pos: Vec3::ZERO, right_plant_pos: Vec3::ZERO }
3983    }
3984
3985    pub fn update(&mut self, pose: &[Transform], prev_pose: &[Transform]) {
3986        let left_vel = if (self.left_foot_bone as usize) < pose.len() {
3987            let prev = prev_pose.get(self.left_foot_bone as usize).copied().unwrap_or_default();
3988            position_error_l2(pose[self.left_foot_bone as usize].position, prev.position)
3989        } else { 1.0 };
3990        let right_vel = if (self.right_foot_bone as usize) < pose.len() {
3991            let prev = prev_pose.get(self.right_foot_bone as usize).copied().unwrap_or_default();
3992            position_error_l2(pose[self.right_foot_bone as usize].position, prev.position)
3993        } else { 1.0 };
3994        if left_vel < self.left_plant_threshold && !self.left_planted {
3995            self.left_planted = true;
3996            self.left_plant_pos = pose.get(self.left_foot_bone as usize).map_or(Vec3::ZERO, |t| t.position);
3997        } else if left_vel >= self.left_plant_threshold { self.left_planted = false; }
3998        if right_vel < self.right_plant_threshold && !self.right_planted {
3999            self.right_planted = true;
4000            self.right_plant_pos = pose.get(self.right_foot_bone as usize).map_or(Vec3::ZERO, |t| t.position);
4001        } else if right_vel >= self.right_plant_threshold { self.right_planted = false; }
4002    }
4003}
4004
4005// ─── Extended: FABRIK IK Chain ────────────────────────────────────────────────
4006
4007pub struct FabrikChain {
4008    pub bone_indices: Vec<u32>,
4009    pub target: Vec3,
4010    pub iterations: usize,
4011    pub tolerance: f32,
4012    pub bone_lengths: Vec<f32>,
4013}
4014
4015impl FabrikChain {
4016    pub fn new(bone_indices: Vec<u32>, bone_lengths: Vec<f32>, target: Vec3) -> Self {
4017        Self { bone_indices, target, iterations: 10, tolerance: 0.001, bone_lengths }
4018    }
4019
4020    pub fn solve(&self, pose: &mut Vec<Transform>) {
4021        let n = self.bone_indices.len();
4022        if n == 0 || n > self.bone_lengths.len() + 1 { return; }
4023        let mut positions: Vec<Vec3> = self.bone_indices.iter()
4024            .map(|&bi| pose.get(bi as usize).map_or(Vec3::ZERO, |t| t.position))
4025            .collect();
4026        let root = positions[0];
4027        let total_len: f32 = self.bone_lengths.iter().sum();
4028        let dist = (self.target - root).length();
4029        if dist > total_len {
4030            let dir = (self.target - root).normalize();
4031            for i in 1..n { positions[i] = positions[i-1] + dir * self.bone_lengths[i-1]; }
4032        } else {
4033            for _ in 0..self.iterations {
4034                positions[n-1] = self.target;
4035                for i in (0..n-1).rev() {
4036                    let dir = (positions[i] - positions[i+1]).normalize();
4037                    positions[i] = positions[i+1] + dir * self.bone_lengths[i];
4038                }
4039                positions[0] = root;
4040                for i in 0..n-1 {
4041                    let dir = (positions[i+1] - positions[i]).normalize();
4042                    positions[i+1] = positions[i] + dir * self.bone_lengths[i];
4043                }
4044                if (positions[n-1] - self.target).length() < self.tolerance { break; }
4045            }
4046        }
4047        for i in 0..n-1 {
4048            let bi = self.bone_indices[i] as usize;
4049            if bi >= pose.len() { continue; }
4050            let dir = (positions[i+1] - positions[i]).normalize();
4051            pose[bi].position = positions[i];
4052            pose[bi].rotation = Quat::from_rotation_arc(Vec3::Y, dir);
4053        }
4054    }
4055}
4056
4057// ─── Extended: Serialization ──────────────────────────────────────────────────
4058
4059pub fn serialize_pose(pose: &[Transform]) -> Vec<u8> {
4060    let mut out = Vec::with_capacity(4 + pose.len() * 40);
4061    out.extend_from_slice(&(pose.len() as u32).to_le_bytes());
4062    for t in pose {
4063        out.extend_from_slice(&t.position.x.to_bits().to_le_bytes());
4064        out.extend_from_slice(&t.position.y.to_bits().to_le_bytes());
4065        out.extend_from_slice(&t.position.z.to_bits().to_le_bytes());
4066        out.extend_from_slice(&t.rotation.x.to_bits().to_le_bytes());
4067        out.extend_from_slice(&t.rotation.y.to_bits().to_le_bytes());
4068        out.extend_from_slice(&t.rotation.z.to_bits().to_le_bytes());
4069        out.extend_from_slice(&t.rotation.w.to_bits().to_le_bytes());
4070        out.extend_from_slice(&t.scale.x.to_bits().to_le_bytes());
4071        out.extend_from_slice(&t.scale.y.to_bits().to_le_bytes());
4072        out.extend_from_slice(&t.scale.z.to_bits().to_le_bytes());
4073    }
4074    out
4075}
4076
4077pub fn deserialize_pose(data: &[u8]) -> Option<Vec<Transform>> {
4078    if data.len() < 4 { return None; }
4079    let n = u32::from_le_bytes(data[0..4].try_into().ok()?) as usize;
4080    if data.len() < 4 + n * 40 { return None; }
4081    let mut pose = Vec::with_capacity(n);
4082    for i in 0..n {
4083        let b = 4 + i * 40;
4084        let r = |s: usize| f32::from_bits(u32::from_le_bytes(data[s..s+4].try_into().unwrap_or([0u8;4])));
4085        pose.push(Transform {
4086            position: Vec3::new(r(b), r(b+4), r(b+8)),
4087            rotation: Quat::from_xyzw(r(b+12), r(b+16), r(b+20), r(b+24)),
4088            scale: Vec3::new(r(b+28), r(b+32), r(b+36)),
4089        });
4090    }
4091    Some(pose)
4092}
4093
4094// ─── Extended: Animation Metrics ─────────────────────────────────────────────
4095
4096#[derive(Debug, Clone)]
4097pub struct AnimationMetrics {
4098    pub clip_name: String,
4099    pub total_frames: u32,
4100    pub active_bones: usize,
4101    pub total_keyframes: usize,
4102    pub avg_keyframes_per_bone: f32,
4103    pub duration_secs: f32,
4104    pub has_root_motion: bool,
4105    pub max_bone_velocity: f32,
4106    pub root_displacement: Vec3,
4107}
4108
4109impl AnimationMetrics {
4110    pub fn compute(clip: &AnimationClip) -> Self {
4111        let total_kf: usize = clip.tracks.iter().map(|t| t.keyframes.len()).sum();
4112        let active = clip.tracks.len();
4113        let root_disp = clip.tracks.first().map(|track| {
4114            let fp = track.keyframes.first().map_or(Vec3::ZERO, |k| k.transform.position);
4115            let lp = track.keyframes.last().map_or(Vec3::ZERO, |k| k.transform.position);
4116            lp - fp
4117        }).unwrap_or(Vec3::ZERO);
4118        let mut max_vel = 0.0f32;
4119        for track in &clip.tracks {
4120            for i in 1..track.keyframes.len() {
4121                let dt = track.keyframes[i].time - track.keyframes[i-1].time;
4122                if dt < 1e-9 { continue; }
4123                let v = (track.keyframes[i].transform.position - track.keyframes[i-1].transform.position).length() / dt;
4124                if v > max_vel { max_vel = v; }
4125            }
4126        }
4127        Self {
4128            clip_name: clip.name.clone(),
4129            total_frames: (clip.duration * clip.frame_rate) as u32,
4130            active_bones: active,
4131            total_keyframes: total_kf,
4132            avg_keyframes_per_bone: if active > 0 { total_kf as f32 / active as f32 } else { 0.0 },
4133            duration_secs: clip.duration,
4134            has_root_motion: root_disp.length() > 0.01,
4135            max_bone_velocity: max_vel,
4136            root_displacement: root_disp,
4137        }
4138    }
4139}
4140
4141// ─── Extended: Skeleton Mask ──────────────────────────────────────────────────
4142
4143#[derive(Debug, Clone)]
4144pub struct SkeletonMask {
4145    pub bone_weights: Vec<f32>,
4146}
4147
4148impl SkeletonMask {
4149    pub fn new(n: usize) -> Self { Self { bone_weights: vec![1.0; n] } }
4150
4151    pub fn upper_body(n: usize, upper_start: usize) -> Self {
4152        let mut m = Self::new(n);
4153        for i in 0..upper_start.min(n) { m.bone_weights[i] = 0.0; }
4154        m
4155    }
4156
4157    pub fn lower_body(n: usize, upper_start: usize) -> Self {
4158        let mut m = Self::new(n);
4159        for i in upper_start..n { m.bone_weights[i] = 0.0; }
4160        m
4161    }
4162
4163    pub fn apply(&self, base: &[Transform], layer: &[Transform]) -> Vec<Transform> {
4164        base.iter().zip(layer.iter()).enumerate().map(|(i, (b, l))| {
4165            b.lerp(l, self.bone_weights.get(i).copied().unwrap_or(0.0))
4166        }).collect()
4167    }
4168}
4169
4170// ─── Extended: Compressed Pose Stream ────────────────────────────────────────
4171
4172pub struct CompressedPoseStream {
4173    pub bone_count: usize,
4174    pub pos_bounds: Vec<PositionBounds>,
4175    pub frames: Vec<Vec<CompressedKeyframe>>,
4176}
4177
4178impl CompressedPoseStream {
4179    pub fn new(bone_count: usize) -> Self {
4180        Self {
4181            bone_count,
4182            pos_bounds: vec![PositionBounds { min: Vec3::splat(-10.0), max: Vec3::splat(10.0) }; bone_count],
4183            frames: Vec::new(),
4184        }
4185    }
4186
4187    pub fn push_pose(&mut self, pose: &[Transform]) {
4188        let frame: Vec<CompressedKeyframe> = (0..self.bone_count.min(pose.len())).map(|bi| {
4189            CompressedKeyframe {
4190                time_ticks: 0,
4191                position: quantize_position_16(pose[bi].position, &self.pos_bounds[bi]),
4192                rotation: compress_quat_smallest3(pose[bi].rotation),
4193                scale: quantize_scale_vec_log8(pose[bi].scale),
4194            }
4195        }).collect();
4196        self.frames.push(frame);
4197    }
4198
4199    pub fn decode_frame(&self, fi: usize) -> Option<Vec<Transform>> {
4200        let frame = self.frames.get(fi)?;
4201        Some(frame.iter().enumerate().map(|(bi, ckf)| Transform {
4202            position: dequantize_position_16(ckf.position, &self.pos_bounds[bi]),
4203            rotation: decompress_quat_smallest3(&ckf.rotation),
4204            scale: dequantize_scale_vec_log8(ckf.scale),
4205        }).collect())
4206    }
4207
4208    pub fn byte_size(&self) -> usize { self.frames.iter().map(|f| f.len() * 21).sum() }
4209}
4210
4211// ─── Extended: Animation LOD Manager ─────────────────────────────────────────
4212
4213pub struct AnimationLodManager {
4214    pub lod_distances: [f32; MAX_LOD_LEVELS],
4215    pub update_rates: [f32; MAX_LOD_LEVELS],
4216    pub current_lods: HashMap<u32, usize>,
4217}
4218
4219impl AnimationLodManager {
4220    pub fn new() -> Self {
4221        Self {
4222            lod_distances: [10.0, 25.0, 60.0, 150.0],
4223            update_rates: [60.0, 30.0, 15.0, 5.0],
4224            current_lods: HashMap::new(),
4225        }
4226    }
4227
4228    pub fn update_entity(&mut self, entity_id: u32, distance: f32) {
4229        let lod = self.lod_distances.iter().position(|&d| distance < d).unwrap_or(MAX_LOD_LEVELS - 1);
4230        self.current_lods.insert(entity_id, lod);
4231    }
4232
4233    pub fn should_update(&self, entity_id: u32, frame: u32) -> bool {
4234        let lod = self.current_lods.get(&entity_id).copied().unwrap_or(0);
4235        let period = (60.0 / self.update_rates[lod]) as u32;
4236        frame % period.max(1) == 0
4237    }
4238
4239    pub fn bone_mask(&self, entity_id: u32) -> u64 {
4240        match self.current_lods.get(&entity_id).copied().unwrap_or(0) {
4241            0 => u64::MAX,
4242            1 => 0x00FFFFFFFFFFFFFF,
4243            2 => 0x000000FFFFFFFFFF,
4244            _ => 0x000000000000FFFF,
4245        }
4246    }
4247}
4248
4249// ─── Extended: Additive Blend Tree ───────────────────────────────────────────
4250
4251pub struct AdditiveBlendTree {
4252    pub base_clip: String,
4253    pub additive_layers: Vec<(String, f32, SkeletonMask)>,
4254}
4255
4256impl AdditiveBlendTree {
4257    pub fn new(base_clip: &str) -> Self {
4258        Self { base_clip: base_clip.to_owned(), additive_layers: Vec::new() }
4259    }
4260
4261    pub fn add_layer(&mut self, clip: &str, weight: f32, mask: SkeletonMask) {
4262        self.additive_layers.push((clip.to_owned(), weight, mask));
4263    }
4264
4265    pub fn evaluate(&self, registry: &AnimationRegistry, time: f32, n_bones: usize) -> Vec<Transform> {
4266        let ev = PoseEvaluator::new(n_bones);
4267        let mut pose = if let Some(clip) = registry.get_compressed(&self.base_clip) {
4268            ev.evaluate_compressed(clip, time)
4269        } else if let Some(clip) = registry.get_clip(&self.base_clip) {
4270            ev.evaluate(clip, time)
4271        } else {
4272            vec![Transform::identity(); n_bones]
4273        };
4274
4275        for (clip_name, weight, mask) in &self.additive_layers {
4276            let add_pose = if let Some(clip) = registry.get_compressed(clip_name) {
4277                ev.evaluate_compressed(clip, time)
4278            } else if let Some(clip) = registry.get_clip(clip_name) {
4279                ev.evaluate(clip, time)
4280            } else {
4281                continue;
4282            };
4283            let blended = mask.apply(&pose, &add_pose);
4284            for (b, bl) in pose.iter_mut().zip(blended.into_iter()) {
4285                *b = b.lerp(&bl, *weight);
4286            }
4287        }
4288        pose
4289    }
4290}
4291
4292// ─── Extended: Per-frame Pose Correction ─────────────────────────────────────
4293
4294pub fn clamp_velocity_per_frame(
4295    pose: &mut Vec<Transform>,
4296    prev_pose: &[Transform],
4297    max_pos_vel: f32,
4298    max_rot_vel: f32,
4299    dt: f32,
4300) {
4301    for (i, t) in pose.iter_mut().enumerate() {
4302        let prev = prev_pose.get(i).copied().unwrap_or_default();
4303        let pos_vel = (t.position - prev.position).length() / dt.max(1e-9);
4304        if pos_vel > max_pos_vel {
4305            let clamped_step = (t.position - prev.position).normalize() * max_pos_vel * dt;
4306            t.position = prev.position + clamped_step;
4307        }
4308        let rot_vel = rotation_error_geodesic(t.rotation, prev.rotation) / dt.max(1e-9);
4309        if rot_vel > max_rot_vel {
4310            let blend_t = max_rot_vel * dt / rot_vel.max(1e-9);
4311            t.rotation = prev.rotation.slerp(t.rotation, blend_t.min(1.0));
4312        }
4313    }
4314}
4315
4316// ─── Extended: Clip Metadata ──────────────────────────────────────────────────
4317
4318#[derive(Debug, Clone)]
4319pub struct ClipMetadata {
4320    pub name: String,
4321    pub category: String,
4322    pub tags: Vec<String>,
4323    pub created_at: u64,
4324    pub author: String,
4325    pub source_file: String,
4326    pub frame_rate: f32,
4327    pub duration: f32,
4328    pub looping: bool,
4329    pub has_root_motion: bool,
4330    pub compression_ratio: f32,
4331}
4332
4333impl ClipMetadata {
4334    pub fn from_clip(clip: &AnimationClip, compressed: &CompressedAnimationClip) -> Self {
4335        let metrics = AnimationMetrics::compute(clip);
4336        Self {
4337            name: clip.name.clone(),
4338            category: String::new(),
4339            tags: Vec::new(),
4340            created_at: 0,
4341            author: String::new(),
4342            source_file: String::new(),
4343            frame_rate: clip.frame_rate,
4344            duration: clip.duration,
4345            looping: clip.looping,
4346            has_root_motion: metrics.has_root_motion,
4347            compression_ratio: compressed.compression_ratio(),
4348        }
4349    }
4350}
4351
4352// ─── Extended: Clip Library ───────────────────────────────────────────────────
4353
4354pub struct ClipLibrary {
4355    pub clips: BTreeMap<String, AnimationClip>,
4356    pub metadata: BTreeMap<String, ClipMetadata>,
4357    pub compressed: BTreeMap<String, CompressedAnimationClip>,
4358    pub compressor: AnimationCompressor,
4359}
4360
4361impl ClipLibrary {
4362    pub fn new() -> Self {
4363        Self {
4364            clips: BTreeMap::new(),
4365            metadata: BTreeMap::new(),
4366            compressed: BTreeMap::new(),
4367            compressor: AnimationCompressor::new(),
4368        }
4369    }
4370
4371    pub fn import(&mut self, clip: AnimationClip) {
4372        let name = clip.name.clone();
4373        let c = self.compressor.compress(&clip);
4374        let meta = ClipMetadata::from_clip(&clip, &c);
4375        self.clips.insert(name.clone(), clip);
4376        self.compressed.insert(name.clone(), c);
4377        self.metadata.insert(name, meta);
4378    }
4379
4380    pub fn search_by_tag(&self, tag: &str) -> Vec<&ClipMetadata> {
4381        self.metadata.values().filter(|m| m.tags.iter().any(|t| t == tag)).collect()
4382    }
4383
4384    pub fn search_by_duration(&self, min: f32, max: f32) -> Vec<&ClipMetadata> {
4385        self.metadata.values().filter(|m| m.duration >= min && m.duration <= max).collect()
4386    }
4387
4388    pub fn total_compressed_size(&self) -> usize {
4389        self.compressed.values().map(|c| c.compressed_byte_size).sum()
4390    }
4391}
4392
4393// ─── Extended: Interpolation Mode Tracks ─────────────────────────────────────
4394
4395#[derive(Debug, Clone, Copy, PartialEq)]
4396pub enum InterpolationMode { Step, Linear, CubicHermite, CatmullRom }
4397
4398pub fn sample_track_with_mode(track: &BoneTrack, t: f32, mode: InterpolationMode) -> Option<Transform> {
4399    if track.keyframes.is_empty() { return None; }
4400    if track.keyframes.len() == 1 { return Some(track.keyframes[0].transform); }
4401    let tc = t.clamp(track.keyframes.first().unwrap().time, track.keyframes.last().unwrap().time);
4402    let idx = track.keyframes.partition_point(|kf| kf.time <= tc).min(track.keyframes.len() - 1).max(1);
4403    let kf0 = &track.keyframes[idx - 1];
4404    let kf1 = &track.keyframes[idx];
4405    let dt = kf1.time - kf0.time;
4406    let alpha = if dt > 1e-9 { (tc - kf0.time) / dt } else { 0.0 };
4407    match mode {
4408        InterpolationMode::Step => Some(kf0.transform),
4409        InterpolationMode::Linear => Some(kf0.transform.lerp(&kf1.transform, alpha)),
4410        InterpolationMode::CubicHermite | InterpolationMode::CatmullRom => {
4411            let tan0 = if idx >= 2 {
4412                let prev = &track.keyframes[idx-2];
4413                let dtp = kf0.time - prev.time;
4414                if dtp > 1e-9 { (kf1.transform.position - prev.transform.position) / (dtp + dt) } else { Vec3::ZERO }
4415            } else { if dt > 1e-9 { (kf1.transform.position - kf0.transform.position) / dt } else { Vec3::ZERO } };
4416            let tan1 = if idx < track.keyframes.len() - 1 {
4417                let next = &track.keyframes[idx+1];
4418                let dtn = next.time - kf1.time;
4419                if dtn > 1e-9 { (next.transform.position - kf0.transform.position) / (dt + dtn) } else { Vec3::ZERO }
4420            } else { if dt > 1e-9 { (kf1.transform.position - kf0.transform.position) / dt } else { Vec3::ZERO } };
4421            let seg = HermiteSegment { t0: kf0.time, t1: kf1.time, p0: kf0.transform.position, p1: kf1.transform.position, m0: tan0, m1: tan1 };
4422            Some(Transform { position: seg.evaluate(tc), rotation: kf0.transform.rotation.slerp(kf1.transform.rotation, alpha), scale: kf0.transform.scale.lerp(kf1.transform.scale, alpha) })
4423        }
4424    }
4425}
4426
4427// ─── Extended: Compression Quality Analysis ───────────────────────────────────
4428
4429pub struct QualityAnalyzer {
4430    pub sample_rate: f32,
4431    pub test_cases: Vec<(AnimationClip, CompressionSettings)>,
4432}
4433
4434impl QualityAnalyzer {
4435    pub fn new(sample_rate: f32) -> Self {
4436        Self { sample_rate, test_cases: Vec::new() }
4437    }
4438
4439    pub fn add_test(&mut self, clip: AnimationClip, settings: CompressionSettings) {
4440        self.test_cases.push((clip, settings));
4441    }
4442
4443    pub fn run_all(&self) -> Vec<CompressionErrorReport> {
4444        self.test_cases.iter().map(|(clip, settings)| {
4445            let compressor = AnimationCompressor::new().with_settings(settings.clone());
4446            let compressed = compressor.compress(clip);
4447            compute_error_report(clip, &compressed)
4448        }).collect()
4449    }
4450
4451    pub fn worst_case<'a>(&self, reports: &'a [CompressionErrorReport]) -> Option<&'a CompressionErrorReport> {
4452        reports.iter().max_by(|a, b| a.global_max_pos_error.partial_cmp(&b.global_max_pos_error).unwrap_or(std::cmp::Ordering::Equal))
4453    }
4454
4455    pub fn passes_threshold(&self, reports: &[CompressionErrorReport], max_pos: f32, max_rot: f32) -> bool {
4456        reports.iter().all(|r| r.global_max_pos_error <= max_pos && r.global_max_rot_error <= max_rot)
4457    }
4458}
4459
4460// ─── Extended: Procedural Breathing ──────────────────────────────────────────
4461
4462pub struct ProceduralBreathing {
4463    pub chest_bone: u32,
4464    pub spine_bone: u32,
4465    pub rate: f32,
4466    pub intensity: f32,
4467    pub phase: f32,
4468}
4469
4470impl ProceduralBreathing {
4471    pub fn new(chest_bone: u32, spine_bone: u32) -> Self {
4472        Self { chest_bone, spine_bone, rate: 0.25, intensity: 0.03, phase: 0.0 }
4473    }
4474
4475    pub fn update(&mut self, dt: f32) {
4476        self.phase = (self.phase + dt * self.rate * std::f32::consts::TAU).rem_euclid(std::f32::consts::TAU);
4477    }
4478
4479    pub fn apply(&self, pose: &mut Vec<Transform>) {
4480        let t = self.phase.sin() * 0.5 + 0.5;
4481        let sa = t * self.intensity;
4482        if (self.chest_bone as usize) < pose.len() {
4483            pose[self.chest_bone as usize].scale += Vec3::new(sa * 0.5, sa, sa * 0.5);
4484        }
4485        if (self.spine_bone as usize) < pose.len() {
4486            let bend_rot = Quat::from_rotation_x(t * self.intensity * 0.5);
4487            pose[self.spine_bone as usize].rotation = (pose[self.spine_bone as usize].rotation * bend_rot).normalize();
4488        }
4489    }
4490}
4491
4492// ─── Extended: Head Look ─────────────────────────────────────────────────────
4493
4494pub struct ProceduralHeadLook {
4495    pub head_bone: u32,
4496    pub neck_bone: u32,
4497    pub target: Vec3,
4498    pub blend: f32,
4499    pub max_angle: f32,
4500    pub current_blend: f32,
4501}
4502
4503impl ProceduralHeadLook {
4504    pub fn new(head: u32, neck: u32) -> Self {
4505        Self { head_bone: head, neck_bone: neck, target: Vec3::ZERO, blend: 1.0, max_angle: std::f32::consts::FRAC_PI_2, current_blend: 0.0 }
4506    }
4507
4508    pub fn update(&mut self, dt: f32) {
4509        self.current_blend = (self.current_blend + dt * 3.0).min(self.blend);
4510    }
4511
4512    pub fn apply(&self, pose: &mut Vec<Transform>) {
4513        if (self.head_bone as usize) >= pose.len() { return; }
4514        let head = pose[self.head_bone as usize];
4515        let dir = self.target - head.position;
4516        if dir.length_squared() < 1e-9 { return; }
4517        let new_rot = Quat::from_rotation_arc(Vec3::Z, dir.normalize());
4518        pose[self.head_bone as usize].rotation = head.rotation.slerp(new_rot, self.current_blend);
4519    }
4520}
4521
4522// ─── End of File ──────────────────────────────────────────────────────────────