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