Skip to main content

token_value_map/time_data_map/
mod.rs

1use crate::*;
2use enum_dispatch::enum_dispatch;
3
4use std::{
5    collections::BTreeMap,
6    iter::FromIterator,
7    ops::{Add, Mul, Sub},
8};
9
10mod sample;
11pub use sample::*;
12
13/// A mapping from time to data values with interpolation support.
14///
15/// Stores time-value pairs in a [`BTreeMap`] for efficient time-based
16/// queries and supports various interpolation methods.
17///
18/// When the `interpolation` feature is enabled, each value can have an
19/// associated interpolation specification for animation curves.
20///
21/// Without `interpolation` feature: `BTreeMap<Time, T>`.
22///
23/// With `interpolation` feature an entry supports a [`Key`]s:
24/// `BTreeMap<Time, (T, Option<Key<T>>)>`.
25#[derive(Clone, Debug, PartialEq, Hash)]
26#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
27#[cfg_attr(feature = "facet", derive(Facet))]
28#[cfg_attr(feature = "facet", facet(opaque))]
29#[cfg_attr(feature = "rkyv", derive(Archive, RkyvSerialize, RkyvDeserialize))]
30pub struct TimeDataMap<T> {
31    /// The time-value pairs with optional interpolation keys.
32    #[cfg(not(feature = "interpolation"))]
33    pub values: BTreeMap<Time, T>,
34    #[cfg(feature = "interpolation")]
35    pub values: BTreeMap<Time, (T, Option<crate::Key<T>>)>,
36}
37
38// Manual Eq implementation.
39impl<T: Eq> Eq for TimeDataMap<T> {}
40
41// AsRef implementation for backward compatibility.
42#[cfg(not(feature = "interpolation"))]
43impl<T> AsRef<BTreeMap<Time, T>> for TimeDataMap<T> {
44    fn as_ref(&self) -> &BTreeMap<Time, T> {
45        &self.values
46    }
47}
48
49// AIDEV-NOTE: These methods provide backward compatibility for code that
50// previously accessed the BTreeMap directly via .0 field.
51// With interpolation feature, these return a view without interpolation specs.
52impl<T> TimeDataMap<T> {
53    /// Get an iterator over time-value pairs.
54    #[inline]
55    pub fn iter(&self) -> impl Iterator<Item = (&Time, &T)> {
56        #[cfg(not(feature = "interpolation"))]
57        {
58            self.values.iter()
59        }
60        #[cfg(feature = "interpolation")]
61        {
62            self.values.iter().map(|(t, (v, _))| (t, v))
63        }
64    }
65
66    /// Check if empty.
67    #[inline]
68    pub fn is_empty(&self) -> bool {
69        self.values.is_empty()
70    }
71
72    /// Get the number of entries.
73    #[inline]
74    pub fn len(&self) -> usize {
75        self.values.len()
76    }
77
78    /// Remove a sample at the given time.
79    ///
80    /// Returns the removed value if it existed.
81    #[inline]
82    pub fn remove(&mut self, time: &Time) -> Option<T> {
83        #[cfg(not(feature = "interpolation"))]
84        {
85            self.values.remove(time)
86        }
87        #[cfg(feature = "interpolation")]
88        {
89            self.values.remove(time).map(|(v, _)| v)
90        }
91    }
92}
93
94// Constructor for backward compatibility.
95impl<T> From<BTreeMap<Time, T>> for TimeDataMap<T> {
96    fn from(values: BTreeMap<Time, T>) -> Self {
97        #[cfg(not(feature = "interpolation"))]
98        {
99            Self { values }
100        }
101        #[cfg(feature = "interpolation")]
102        {
103            Self {
104                values: values.into_iter().map(|(k, v)| (k, (v, None))).collect(),
105            }
106        }
107    }
108}
109
110/// Control operations `trait` for time data maps.
111#[enum_dispatch]
112pub trait TimeDataMapControl<T> {
113    /// Returns the number of time samples.
114    fn len(&self) -> usize;
115    /// Returns `true` if there are no time samples.
116    fn is_empty(&self) -> bool;
117    /// Returns `true` if there is more than one time sample.
118    fn is_animated(&self) -> bool;
119}
120
121impl<T> TimeDataMapControl<T> for TimeDataMap<T> {
122    fn len(&self) -> usize {
123        self.values.len()
124    }
125
126    fn is_empty(&self) -> bool {
127        self.values.is_empty()
128    }
129
130    fn is_animated(&self) -> bool {
131        self.values.len() > 1
132    }
133}
134
135#[cfg(feature = "builtin-types")]
136impl<T> crate::DataTypeOps for TimeDataMap<T>
137where
138    T: crate::DataTypeOps,
139{
140    fn data_type(&self) -> crate::DataType {
141        // Use the first element to determine the type, or return a default if empty.
142        #[cfg(not(feature = "interpolation"))]
143        {
144            self.values
145                .values()
146                .next()
147                .map(|v| v.data_type())
148                .unwrap_or(DataType::Real)
149        }
150        #[cfg(feature = "interpolation")]
151        {
152            self.values
153                .values()
154                .next()
155                .map(|(v, _)| v.data_type())
156                .unwrap_or(DataType::Real)
157        }
158    }
159
160    fn type_name(&self) -> &'static str {
161        // Use the first element to determine the type name.
162        #[cfg(not(feature = "interpolation"))]
163        {
164            self.values
165                .values()
166                .next()
167                .map(|v| v.type_name())
168                .unwrap_or("unknown")
169        }
170        #[cfg(feature = "interpolation")]
171        {
172            self.values
173                .values()
174                .next()
175                .map(|(v, _)| v.type_name())
176                .unwrap_or("unknown")
177        }
178    }
179}
180
181// AIDEV-NOTE: These From impls are only available with builtin-types feature.
182#[cfg(feature = "builtin-types")]
183macro_rules! impl_from_at_time {
184    ($($t:ty),+) => {
185        $(
186            impl From<(Time, $t)> for TimeDataMap<$t> {
187                fn from((time, value): (Time, $t)) -> Self {
188                    TimeDataMap::from(BTreeMap::from([(time, value)]))
189                }
190            }
191        )+
192    };
193}
194
195#[cfg(feature = "builtin-types")]
196impl_from_at_time!(
197    Boolean, Real, Integer, String, Color, BooleanVec, RealVec, IntegerVec, StringVec, ColorVec,
198    Data
199);
200
201#[cfg(all(feature = "builtin-types", feature = "vector2"))]
202impl_from_at_time!(Vector2);
203#[cfg(all(feature = "builtin-types", feature = "vector3"))]
204impl_from_at_time!(Vector3);
205#[cfg(all(feature = "builtin-types", feature = "matrix3"))]
206impl_from_at_time!(Matrix3);
207#[cfg(all(feature = "builtin-types", feature = "normal3"))]
208impl_from_at_time!(Normal3);
209#[cfg(all(feature = "builtin-types", feature = "point3"))]
210impl_from_at_time!(Point3);
211#[cfg(all(feature = "builtin-types", feature = "matrix4"))]
212impl_from_at_time!(Matrix4);
213
214#[cfg(all(
215    feature = "builtin-types",
216    feature = "vector2",
217    feature = "vec_variants"
218))]
219impl_from_at_time!(Vector2Vec);
220#[cfg(all(
221    feature = "builtin-types",
222    feature = "vector3",
223    feature = "vec_variants"
224))]
225impl_from_at_time!(Vector3Vec);
226#[cfg(all(
227    feature = "builtin-types",
228    feature = "matrix3",
229    feature = "vec_variants"
230))]
231impl_from_at_time!(Matrix3Vec);
232#[cfg(all(
233    feature = "builtin-types",
234    feature = "normal3",
235    feature = "vec_variants"
236))]
237impl_from_at_time!(Normal3Vec);
238#[cfg(all(
239    feature = "builtin-types",
240    feature = "point3",
241    feature = "vec_variants"
242))]
243impl_from_at_time!(Point3Vec);
244#[cfg(all(
245    feature = "builtin-types",
246    feature = "matrix4",
247    feature = "vec_variants"
248))]
249impl_from_at_time!(Matrix4Vec);
250
251impl<T> TimeDataMap<T> {
252    /// Insert a value at the given time.
253    pub fn insert(&mut self, time: Time, value: T) {
254        #[cfg(not(feature = "interpolation"))]
255        {
256            self.values.insert(time, value);
257        }
258        #[cfg(feature = "interpolation")]
259        {
260            self.values.insert(time, (value, None));
261        }
262    }
263
264    /// Get a value at the exact time.
265    pub fn get(&self, time: &Time) -> Option<&T> {
266        #[cfg(not(feature = "interpolation"))]
267        {
268            self.values.get(time)
269        }
270        #[cfg(feature = "interpolation")]
271        {
272            self.values.get(time).map(|(v, _)| v)
273        }
274    }
275}
276
277impl<T> TimeDataMap<T>
278where
279    T: Clone + Add<Output = T> + Mul<f32, Output = T> + Sub<Output = T>,
280{
281    pub fn interpolate(&self, time: Time) -> T {
282        #[cfg(not(feature = "interpolation"))]
283        {
284            interpolate(&self.values, time)
285        }
286        #[cfg(feature = "interpolation")]
287        {
288            interpolate_with_specs(&self.values, time)
289        }
290    }
291}
292
293// Interpolation-specific methods.
294#[cfg(feature = "interpolation")]
295impl<T> TimeDataMap<T> {
296    /// Insert a value with interpolation specification.
297    ///
298    /// Sets both the value at the given time and how it should interpolate
299    /// to neighboring keyframes.
300    pub fn insert_with_interpolation(&mut self, time: Time, value: T, spec: crate::Key<T>) {
301        self.values.insert(time, (value, Some(spec)));
302    }
303
304    /// Get interpolation spec at a given time.
305    ///
306    /// Returns `None` if no interpolation metadata exists or no spec at this time.
307    pub fn interpolation(&self, time: &Time) -> Option<&crate::Key<T>> {
308        self.values.get(time)?.1.as_ref()
309    }
310
311    /// Set or update the interpolation spec at a given time.
312    ///
313    /// Returns `Ok(())` if the time existed and was updated, or an error if no
314    /// sample exists at that time.
315    pub fn set_interpolation_at(&mut self, time: &Time, key: crate::Key<T>) -> crate::Result<()> {
316        if let Some(entry) = self.values.get_mut(time) {
317            entry.1 = Some(key);
318            Ok(())
319        } else {
320            Err(crate::Error::TimeNotFound { time: *time })
321        }
322    }
323
324    /// Clear all interpolation metadata.
325    ///
326    /// After calling this, all interpolation reverts to automatic mode.
327    pub fn clear_interpolation(&mut self) {
328        for (_, interp) in self.values.values_mut() {
329            *interp = None;
330        }
331    }
332
333    /// Check if using custom interpolation.
334    ///
335    /// Returns `true` if any interpolation metadata is present.
336    pub fn has_interpolation(&self) -> bool {
337        self.values.values().any(|(_, interp)| interp.is_some())
338    }
339}
340
341impl<T, V> FromIterator<(Time, V)> for TimeDataMap<T>
342where
343    V: Into<T>,
344{
345    fn from_iter<I>(iter: I) -> Self
346    where
347        I: IntoIterator<Item = (Time, V)>,
348    {
349        Self::from(BTreeMap::from_iter(
350            iter.into_iter().map(|(t, v)| (t, v.into())),
351        ))
352    }
353}
354
355impl<T> TimeDataMap<T> {
356    pub fn closest_sample(&self, time: Time) -> &T {
357        #[cfg(not(feature = "interpolation"))]
358        {
359            let mut range = self.values.range(time..);
360            let greater_or_equal = range.next();
361
362            let mut range = self.values.range(..time);
363            let less_than = range.next_back();
364
365            match (less_than, greater_or_equal) {
366                (Some((lower_k, lower_v)), Some((upper_k, upper_v))) => {
367                    if (time.as_ref() - lower_k.as_ref()).abs()
368                        <= (upper_k.as_ref() - time.as_ref()).abs()
369                    {
370                        lower_v
371                    } else {
372                        upper_v
373                    }
374                }
375                (Some(entry), None) | (None, Some(entry)) => entry.1,
376                (None, None) => {
377                    unreachable!("TimeDataMap can never be empty")
378                }
379            }
380        }
381        #[cfg(feature = "interpolation")]
382        {
383            let mut range = self.values.range(time..);
384            let greater_or_equal = range.next();
385
386            let mut range = self.values.range(..time);
387            let less_than = range.next_back();
388
389            match (less_than, greater_or_equal) {
390                (Some((lower_k, (lower_v, _))), Some((upper_k, (upper_v, _)))) => {
391                    if (time.as_ref() - lower_k.as_ref()).abs()
392                        <= (upper_k.as_ref() - time.as_ref()).abs()
393                    {
394                        lower_v
395                    } else {
396                        upper_v
397                    }
398                }
399                (Some((_, (v, _))), None) | (None, Some((_, (v, _)))) => v,
400                (None, None) => {
401                    unreachable!("TimeDataMap can never be empty")
402                }
403            }
404        }
405    }
406
407    /// Sample value at exact time (no interpolation).
408    pub fn sample_at(&self, time: Time) -> Option<&T> {
409        self.get(&time)
410    }
411
412    /// Get the value at or before the given time.
413    pub fn sample_at_or_before(&self, time: Time) -> Option<&T> {
414        #[cfg(not(feature = "interpolation"))]
415        {
416            self.values.range(..=time).next_back().map(|(_, v)| v)
417        }
418        #[cfg(feature = "interpolation")]
419        {
420            self.values.range(..=time).next_back().map(|(_, (v, _))| v)
421        }
422    }
423
424    /// Get the value at or after the given time.
425    pub fn sample_at_or_after(&self, time: Time) -> Option<&T> {
426        #[cfg(not(feature = "interpolation"))]
427        {
428            self.values.range(time..).next().map(|(_, v)| v)
429        }
430        #[cfg(feature = "interpolation")]
431        {
432            self.values.range(time..).next().map(|(_, (v, _))| v)
433        }
434    }
435
436    /// Get surrounding samples for interpolation.
437    ///
438    /// Returns up to `N` samples centered around the given time for
439    /// use in interpolation algorithms.
440    pub fn sample_surrounding<const N: usize>(&self, time: Time) -> SmallVec<[(Time, &T); N]> {
441        // Get samples before the time.
442        let before_count = N / 2;
443
444        #[cfg(not(feature = "interpolation"))]
445        {
446            let mut result = self
447                .values
448                .range(..time)
449                .rev()
450                .take(before_count)
451                .map(|(t, v)| (*t, v))
452                .collect::<SmallVec<[_; N]>>();
453            result.reverse();
454
455            // Get samples at or after the time.
456            let after_count = N - result.len();
457            result.extend(
458                self.values
459                    .range(time..)
460                    .take(after_count)
461                    .map(|(t, v)| (*t, v)),
462            );
463            result
464        }
465
466        #[cfg(feature = "interpolation")]
467        {
468            let mut result = self
469                .values
470                .range(..time)
471                .rev()
472                .take(before_count)
473                .map(|(t, (v, _))| (*t, v))
474                .collect::<SmallVec<[_; N]>>();
475            result.reverse();
476
477            // Get samples at or after the time.
478            let after_count = N - result.len();
479            result.extend(
480                self.values
481                    .range(time..)
482                    .take(after_count)
483                    .map(|(t, (v, _))| (*t, v)),
484            );
485            result
486        }
487    }
488}
489
490// AIDEV-NOTE: Internal types for backend-agnostic Matrix3 SVD decomposition.
491// These implement the arithmetic traits needed by the generic `interpolate()`
492// function, avoiding any dependency on a specific math backend for SVD compute.
493// The analytical 2×2 SVD replaces the previous nalgebra-dependent implementation.
494
495/// Internal 2D translation extracted from a 3×3 homogeneous matrix.
496#[cfg(feature = "matrix3")]
497#[derive(Clone)]
498struct Trans2(f32, f32);
499
500#[cfg(feature = "matrix3")]
501impl Add for Trans2 {
502    type Output = Self;
503    fn add(self, rhs: Self) -> Self {
504        Trans2(self.0 + rhs.0, self.1 + rhs.1)
505    }
506}
507
508#[cfg(feature = "matrix3")]
509impl Sub for Trans2 {
510    type Output = Self;
511    fn sub(self, rhs: Self) -> Self {
512        Trans2(self.0 - rhs.0, self.1 - rhs.1)
513    }
514}
515
516#[cfg(feature = "matrix3")]
517impl Mul<f32> for Trans2 {
518    type Output = Self;
519    fn mul(self, s: f32) -> Self {
520        Trans2(self.0 * s, self.1 * s)
521    }
522}
523
524/// Internal diagonal stretch (singular values) for decomposed 3×3 matrices.
525#[cfg(feature = "matrix3")]
526#[derive(Clone)]
527struct DiagStretch(f32, f32);
528
529#[cfg(feature = "matrix3")]
530impl Add for DiagStretch {
531    type Output = Self;
532    fn add(self, rhs: Self) -> Self {
533        DiagStretch(self.0 + rhs.0, self.1 + rhs.1)
534    }
535}
536
537#[cfg(feature = "matrix3")]
538impl Sub for DiagStretch {
539    type Output = Self;
540    fn sub(self, rhs: Self) -> Self {
541        DiagStretch(self.0 - rhs.0, self.1 - rhs.1)
542    }
543}
544
545#[cfg(feature = "matrix3")]
546impl Mul<f32> for DiagStretch {
547    type Output = Self;
548    fn mul(self, s: f32) -> Self {
549        DiagStretch(self.0 * s, self.1 * s)
550    }
551}
552
553/// Interpolate 2D rotation angles via shortest-path slerp across a [`BTreeMap`].
554#[cfg(feature = "matrix3")]
555#[inline(always)]
556fn interpolate_rotation(map: &BTreeMap<Time, f32>, time: Time) -> f32 {
557    if map.len() == 1 {
558        return *map.values().next().unwrap();
559    }
560
561    let first = map.iter().next().unwrap();
562    let last = map.iter().next_back().unwrap();
563
564    if time <= *first.0 {
565        return *first.1;
566    }
567
568    if time >= *last.0 {
569        return *last.1;
570    }
571
572    let lower = map.range(..=time).next_back().unwrap();
573    let upper = map.range(time..).next().unwrap();
574
575    let t0 = f32::from(*lower.0);
576    let t1 = f32::from(*upper.0);
577    let a0 = *lower.1;
578    let a1 = *upper.1;
579
580    // Normalize `t` to [0, 1].
581    let t: f32 = (f32::from(time) - t0) / (t1 - t0);
582
583    // Shortest-path angle interpolation.
584    use std::f32::consts::{PI, TAU};
585    let mut diff = a1 - a0;
586    if diff > PI {
587        diff -= TAU;
588    } else if diff < -PI {
589        diff += TAU;
590    }
591    a0 + diff * t
592}
593
594// AIDEV-NOTE: Helper functions for converting BezierHandle variants to slopes and evaluating mixed interpolation modes.
595
596#[cfg(feature = "interpolation")]
597fn bezier_handle_to_slope<V>(
598    handle: &crate::BezierHandle<V>,
599    t1: f32,
600    t2: f32,
601    _v1: &V,
602    _v2: &V,
603) -> Option<V>
604where
605    V: Clone + Add<Output = V> + Sub<Output = V> + Mul<f32, Output = V>,
606{
607    match handle {
608        crate::BezierHandle::SlopePerSecond(s) => Some(s.clone()),
609        crate::BezierHandle::SlopePerFrame(s) => {
610            let frames = (t2 - t1).max(f32::EPSILON);
611            Some(s.clone() * (frames / (t2 - t1)))
612        }
613        crate::BezierHandle::Delta { time, value } => {
614            let time_f32: f32 = (*time).into();
615            if time_f32.abs() <= f32::EPSILON {
616                None
617            } else {
618                Some(value.clone() * (1.0 / time_f32))
619            }
620        }
621        crate::BezierHandle::Angle(_) => None, // angle needs higher-level context; fall back later
622    }
623}
624
625#[cfg(feature = "interpolation")]
626#[allow(clippy::too_many_arguments)]
627fn smooth_tangent<V>(
628    t1: f32,
629    v1: &V,
630    t2: f32,
631    v2: &V,
632    map: &BTreeMap<Time, (V, Option<crate::Key<V>>)>,
633    _time: Time,
634    anchor: Time,
635    incoming: bool,
636) -> V
637where
638    V: Clone + Add<Output = V> + Mul<f32, Output = V> + Sub<Output = V>,
639{
640    use smallvec::SmallVec;
641
642    // Minimal Catmull-style tangent: reuse nearby keys via existing helpers.
643    // We reuse interpolate() on a virtual window to let the hermite logic derive slopes.
644    let mut window = SmallVec::<[(Time, V); 2]>::new();
645
646    if incoming {
647        if let Some(prev) = map.range(..anchor).next_back() {
648            window.push((*prev.0, prev.1.0.clone()));
649        }
650        window.push((anchor, v1.clone()));
651    } else {
652        window.push((anchor, v1.clone()));
653        if let Some(next) = map.range(anchor..).nth(1) {
654            window.push((*next.0, next.1.0.clone()));
655        }
656    }
657
658    if window.len() == 2 {
659        let (t0, p0) = &window[0];
660        let (t1, p1) = &window[1];
661        let t0_f: f32 = (*t0).into();
662        let t1_f: f32 = (*t1).into();
663        let dt = (t1_f - t0_f).max(f32::EPSILON);
664        (p1.clone() - p0.clone()) * (1.0 / dt)
665    } else {
666        // Fall back to local linear slope.
667        (v2.clone() - v1.clone()) * (1.0 / (t2 - t1).max(f32::EPSILON))
668    }
669}
670
671#[cfg(feature = "interpolation")]
672fn evaluate_mixed_bezier<V>(
673    time: Time,
674    t1: Time,
675    v1: &V,
676    slope_out: &V,
677    t2: Time,
678    v2: &V,
679    slope_in: &V,
680) -> V
681where
682    V: Clone + Add<Output = V> + Mul<f32, Output = V> + Sub<Output = V>,
683{
684    use crate::interpolation::bezier_helpers::*;
685
686    let (p1, p2) = control_points_from_slopes(t1.into(), v1, slope_out, t2.into(), v2, slope_in);
687
688    evaluate_bezier_component_wise(
689        time.into(),
690        (t1.into(), v1),
691        (p1.0, &p1.1),
692        (p2.0, &p2.1),
693        (t2.into(), v2),
694    )
695}
696
697/// Interpolate with respect to interpolation specifications.
698///
699/// This function checks for custom interpolation specs and uses them if available,
700/// otherwise falls back to automatic interpolation.
701#[cfg(feature = "interpolation")]
702#[inline(always)]
703pub(crate) fn interpolate_with_specs<V>(
704    map: &BTreeMap<Time, (V, Option<crate::Key<V>>)>,
705    time: Time,
706) -> V
707where
708    V: Clone + Add<Output = V> + Mul<f32, Output = V> + Sub<Output = V>,
709{
710    if map.len() == 1 {
711        return map.values().next().unwrap().0.clone();
712    }
713
714    let first = map.iter().next().unwrap();
715    let last = map.iter().next_back().unwrap();
716
717    if time <= *first.0 {
718        return first.1.0.clone();
719    }
720
721    if time >= *last.0 {
722        return last.1.0.clone();
723    }
724
725    // Find surrounding keyframes.
726    let lower = map.range(..time).next_back().unwrap();
727    let upper = map.range(time..).next().unwrap();
728
729    // If we're exactly on a keyframe, return its value.
730    if lower.0 == upper.0 {
731        return lower.1.0.clone();
732    }
733
734    let (t1, (v1, spec1)) = lower;
735    let (t2, (v2, spec2)) = upper;
736
737    // Check if we have interpolation specs.
738    let interp_out = spec1.as_ref().map(|s| &s.interpolation_out);
739    let interp_in = spec2.as_ref().map(|s| &s.interpolation_in);
740
741    // AIDEV-NOTE: Determine interpolation mode based on specs.
742    // Hold takes precedence over everything else, then explicit Bezier handles,
743    // then Smooth (Catmull-Rom style), then Linear, then fallback to automatic.
744    match (interp_out, interp_in) {
745        // Step/hold beats everything else.
746        (Some(crate::Interpolation::Hold), _) | (_, Some(crate::Interpolation::Hold)) => v1.clone(),
747
748        // Both sides supply explicit handles -- run a cubic Bezier with those tangents.
749        (
750            Some(crate::Interpolation::Bezier(out_handle)),
751            Some(crate::Interpolation::Bezier(in_handle)),
752        ) => {
753            use crate::interpolation::bezier_helpers::*;
754
755            if let (Some(slope_out), Some(slope_in)) = (
756                bezier_handle_to_slope(out_handle, (*t1).into(), (*t2).into(), v1, v2),
757                bezier_handle_to_slope(in_handle, (*t1).into(), (*t2).into(), v1, v2),
758            ) {
759                let (p1, p2) = control_points_from_slopes(
760                    (*t1).into(),
761                    v1,
762                    &slope_out,
763                    (*t2).into(),
764                    v2,
765                    &slope_in,
766                );
767
768                evaluate_bezier_component_wise(
769                    time.into(),
770                    ((*t1).into(), v1),
771                    (p1.0, &p1.1),
772                    (p2.0, &p2.1),
773                    ((*t2).into(), v2),
774                )
775            } else {
776                // Fall back to linear if we can't convert handles to slopes.
777                linear_interp(*t1, *t2, v1, v2, time)
778            }
779        }
780
781        // One side explicit, the other "smooth": derive a Catmull-style tangent for the smooth side.
782        (Some(crate::Interpolation::Bezier(out_handle)), Some(crate::Interpolation::Smooth)) => {
783            if let Some(slope_out) =
784                bezier_handle_to_slope(out_handle, (*t1).into(), (*t2).into(), v1, v2)
785            {
786                let slope_in =
787                    smooth_tangent((*t1).into(), v1, (*t2).into(), v2, map, time, *t1, false);
788
789                evaluate_mixed_bezier(time, *t1, v1, &slope_out, *t2, v2, &slope_in)
790            } else {
791                linear_interp(*t1, *t2, v1, v2, time)
792            }
793        }
794        (Some(crate::Interpolation::Smooth), Some(crate::Interpolation::Bezier(in_handle))) => {
795            if let Some(slope_in) =
796                bezier_handle_to_slope(in_handle, (*t1).into(), (*t2).into(), v1, v2)
797            {
798                let slope_out =
799                    smooth_tangent((*t1).into(), v1, (*t2).into(), v2, map, time, *t1, true);
800
801                evaluate_mixed_bezier(time, *t1, v1, &slope_out, *t2, v2, &slope_in)
802            } else {
803                linear_interp(*t1, *t2, v1, v2, time)
804            }
805        }
806
807        // Symmetric "smooth" -> fall back to the existing automatic interpolation.
808        (Some(crate::Interpolation::Smooth), Some(crate::Interpolation::Smooth)) | (None, None) => {
809            let values_only: BTreeMap<Time, V> =
810                map.iter().map(|(k, (v, _))| (*k, v.clone())).collect();
811            interpolate(&values_only, time)
812        }
813
814        // Linear/linear, linear vs smooth, or any unsupported combination -> straight line.
815        _ => linear_interp(*t1, *t2, v1, v2, time),
816    }
817}
818
819#[inline(always)]
820pub(crate) fn interpolate<V>(map: &BTreeMap<Time, V>, time: Time) -> V
821where
822    V: Clone + Add<Output = V> + Mul<f32, Output = V> + Sub<Output = V>,
823{
824    if map.len() == 1 {
825        return map.values().next().unwrap().clone();
826    }
827
828    let first = map.iter().next().unwrap();
829    let last = map.iter().next_back().unwrap();
830
831    if time <= *first.0 {
832        return first.1.clone();
833    }
834
835    if time >= *last.0 {
836        return last.1.clone();
837    }
838
839    let lower = map.range(..time).next_back().unwrap();
840    let upper = map.range(time..).next().unwrap();
841
842    // This is our window for interpolation that holds two to four values.
843    //
844    // The interpolation algorithm is chosen based on how many values are in
845    // the window. See the `match` block below.
846    let mut window = SmallVec::<[(Time, &V); 4]>::new();
847
848    // Extend with up to two values before `lower`.
849    window.extend(map.range(..*lower.0).rev().take(2).map(|(k, v)| (*k, v)));
850
851    // Add `lower` and `upper` (if distinct).
852    window.push((*lower.0, lower.1));
853
854    if lower.0 != upper.0 {
855        window.push((*upper.0, upper.1));
856    }
857
858    // Extend with up to one value after `upper`.
859    window.extend(map.range(*upper.0..).skip(1).take(1).map(|(k, v)| (*k, v)));
860
861    // Ensure chronological order.
862    window.reverse();
863
864    match window.len() {
865        4 => {
866            let (t0, p0) = window[0];
867            let (t1, p1) = window[1];
868            let (t2, p2) = window[2];
869            let (t3, p3) = window[3];
870            hermite_interp(HermiteParams {
871                t0,
872                t1,
873                t2,
874                t3,
875                p0,
876                p1,
877                p2,
878                p3,
879                t: time,
880            })
881        }
882        3 => {
883            let (t0, p0) = window[0];
884            let (t1, p1) = window[1];
885            let (t2, p2) = window[2];
886            quadratic_interp(t0, t1, t2, p0, p1, p2, time)
887        }
888        2 => {
889            let (x0, y0) = window[0];
890            let (x1, y1) = window[1];
891            linear_interp(x0, x1, y0, y1, time)
892        }
893        1 => {
894            // Single keyframe - return its value.
895            window[0].1.clone()
896        }
897        0 => {
898            // This shouldn't happen given the checks above, but be defensive.
899            panic!("Interpolation window is empty - this is a bug in token-value-map")
900        }
901        _ => {
902            // Window has more than 4 elements - shouldn't happen but use first 4.
903            let (t0, p0) = window[0];
904            let (t1, p1) = window[1];
905            let (t2, p2) = window[2];
906            let (t3, p3) = window[3];
907            hermite_interp(HermiteParams {
908                t0,
909                t1,
910                t2,
911                t3,
912                p0,
913                p1,
914                p2,
915                p3,
916                t: time,
917            })
918        }
919    }
920}
921
922#[inline(always)]
923fn linear_interp<V, T>(x0: T, x1: T, y0: &V, y1: &V, x: T) -> V
924where
925    V: Clone + Add<Output = V> + Mul<f32, Output = V> + Sub<Output = V>,
926    T: Into<f32>,
927{
928    let x0 = x0.into();
929    let x1 = x1.into();
930    let x = x.into();
931    let alpha = (x - x0) / (x1 - x0);
932    y0.clone() + (y1.clone() - y0.clone()) * alpha
933}
934
935#[inline(always)]
936fn quadratic_interp<V, T>(x0: T, x1: T, x2: T, y0: &V, y1: &V, y2: &V, x: T) -> V
937where
938    V: Clone + Add<Output = V> + Mul<f32, Output = V> + Sub<Output = V>,
939    T: Into<f32>,
940{
941    let x0 = x0.into();
942    let x1 = x1.into();
943    let x2 = x2.into();
944    let x = x.into();
945    let a = (x - x1) * (x - x2) / ((x0 - x1) * (x0 - x2));
946    let b = (x - x0) * (x - x2) / ((x1 - x0) * (x1 - x2));
947    let c = (x - x0) * (x - x1) / ((x2 - x0) * (x2 - x1));
948
949    y0.clone() * a + y1.clone() * b + y2.clone() * c
950}
951
952struct HermiteParams<V, T> {
953    t0: T,
954    t1: T,
955    t2: T,
956    t3: T,
957    p0: V,
958    p1: V,
959    p2: V,
960    p3: V,
961    t: T,
962}
963
964#[inline(always)]
965fn hermite_interp<V, T>(params: HermiteParams<&V, T>) -> V
966where
967    V: Clone + Add<Output = V> + Mul<f32, Output = V> + Sub<Output = V>,
968    T: Into<f32>,
969{
970    let t0 = params.t0.into();
971    let t1 = params.t1.into();
972    let t2 = params.t2.into();
973    let t3 = params.t3.into();
974    let t = params.t.into();
975
976    // Account for knot positions
977    let tau = if (t2 - t1).abs() < f32::EPSILON {
978        0.0 // Handle degenerate case where t1 == t2
979    } else {
980        (t - t1) / (t2 - t1)
981    };
982
983    // Calculate tension/bias parameters based on the spacing between knots.
984    // This makes the spline respond to actual time intervals rather than
985    // assuming uniform spacing.
986    let tension1 = if (t1 - t0).abs() < f32::EPSILON {
987        1.0
988    } else {
989        (t2 - t1) / (t1 - t0)
990    };
991    let tension2 = if (t3 - t2).abs() < f32::EPSILON {
992        1.0
993    } else {
994        (t2 - t1) / (t3 - t2)
995    };
996
997    // Tangent vectors that respect the actual knot spacing
998    let m1 = (params.p2.clone() - params.p0.clone()) * (0.5 * tension1);
999    let m2 = (params.p3.clone() - params.p1.clone()) * (0.5 * tension2);
1000
1001    // Hermite basis functions
1002    let tau2 = tau * tau;
1003    let tau3 = tau2 * tau;
1004
1005    let h00 = 2.0 * tau3 - 3.0 * tau2 + 1.0;
1006    let h10 = tau3 - 2.0 * tau2 + tau;
1007    let h01 = -2.0 * tau3 + 3.0 * tau2;
1008    let h11 = tau3 - tau2;
1009
1010    // Hermite interpolation with proper tangent vectors
1011    params.p1.clone() * h00 + m1 * h10 + params.p2.clone() * h01 + m2 * h11
1012}
1013
1014/// Analytical 2×2 SVD decomposition of a 3×3 homogeneous matrix.
1015///
1016/// Extracts translation, rotation angle (radians), and diagonal stretch
1017/// (singular values) without depending on any specific math backend.
1018#[cfg(feature = "matrix3")]
1019#[inline(always)]
1020fn decompose_matrix(matrix: &crate::math::Mat3Impl) -> (Trans2, f32, DiagStretch) {
1021    use crate::math::mat3;
1022
1023    // Extract translation from the last column.
1024    let tx = mat3(matrix, 0, 2);
1025    let ty = mat3(matrix, 1, 2);
1026
1027    // Upper-left 2×2 linear block.
1028    let a = mat3(matrix, 0, 0);
1029    let b = mat3(matrix, 0, 1);
1030    let c = mat3(matrix, 1, 0);
1031    let d = mat3(matrix, 1, 1);
1032
1033    // Analytical 2×2 SVD: M = Rot(θ) × diag(σ₁, σ₂) × Rot(-φ).
1034    let e = (a + d) * 0.5;
1035    let f = (a - d) * 0.5;
1036    let g = (c + b) * 0.5;
1037    let h = (c - b) * 0.5;
1038
1039    let q = (e * e + h * h).sqrt();
1040    let r = (f * f + g * g).sqrt();
1041
1042    let s1 = q + r;
1043    let s2 = q - r;
1044
1045    let theta1 = g.atan2(f);
1046    let theta2 = h.atan2(e);
1047
1048    // U rotation angle.
1049    let rotation_angle = (theta2 + theta1) * 0.5;
1050
1051    (Trans2(tx, ty), rotation_angle, DiagStretch(s1, s2))
1052}
1053
1054/// Recompose a 3×3 homogeneous matrix from its decomposed parts.
1055///
1056/// Constructs `Rot(angle) × diag(sx, sy, 1)` with translation in the last column.
1057#[cfg(feature = "matrix3")]
1058#[inline(always)]
1059fn recompose_matrix(
1060    translation: Trans2,
1061    rotation_angle: f32,
1062    stretch: DiagStretch,
1063) -> crate::math::Mat3Impl {
1064    let cos = rotation_angle.cos();
1065    let sin = rotation_angle.sin();
1066
1067    // Rot(θ) × diag(sx, sy, 1) in column-major layout.
1068    crate::math::mat3_from_column_slice(&[
1069        stretch.0 * cos,
1070        stretch.0 * sin,
1071        0.0,
1072        -stretch.1 * sin,
1073        stretch.1 * cos,
1074        0.0,
1075        translation.0,
1076        translation.1,
1077        1.0,
1078    ])
1079}