bevy_math/curve/
cores.rs

1//! Core data structures to be used internally in Curve implementations, encapsulating storage
2//! and access patterns for reuse.
3//!
4//! The `Core` types here expose their fields publicly so that it is easier to manipulate and
5//! extend them, but in doing so, you must maintain the invariants of those fields yourself. The
6//! provided methods all maintain the invariants, so this is only a concern if you manually mutate
7//! the fields.
8
9use super::interval::Interval;
10use core::fmt::Debug;
11use derive_more::derive::{Display, Error};
12use itertools::Itertools;
13
14#[cfg(feature = "bevy_reflect")]
15use bevy_reflect::Reflect;
16
17/// This type expresses the relationship of a value to a fixed collection of values. It is a kind
18/// of summary used intermediately by sampling operations.
19#[derive(Debug, Copy, Clone, PartialEq)]
20#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
21#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
22pub enum InterpolationDatum<T> {
23    /// This value lies exactly on a value in the family.
24    Exact(T),
25
26    /// This value is off the left tail of the family; the inner value is the family's leftmost.
27    LeftTail(T),
28
29    /// This value is off the right tail of the family; the inner value is the family's rightmost.
30    RightTail(T),
31
32    /// This value lies on the interior, in between two points, with a third parameter expressing
33    /// the interpolation factor between the two.
34    Between(T, T, f32),
35}
36
37impl<T> InterpolationDatum<T> {
38    /// Map all values using a given function `f`, leaving the interpolation parameters in any
39    /// [`Between`] variants unchanged.
40    ///
41    /// [`Between`]: `InterpolationDatum::Between`
42    #[must_use]
43    pub fn map<S>(self, f: impl Fn(T) -> S) -> InterpolationDatum<S> {
44        match self {
45            InterpolationDatum::Exact(v) => InterpolationDatum::Exact(f(v)),
46            InterpolationDatum::LeftTail(v) => InterpolationDatum::LeftTail(f(v)),
47            InterpolationDatum::RightTail(v) => InterpolationDatum::RightTail(f(v)),
48            InterpolationDatum::Between(u, v, s) => InterpolationDatum::Between(f(u), f(v), s),
49        }
50    }
51}
52
53/// The data core of a curve derived from evenly-spaced samples. The intention is to use this
54/// in addition to explicit or inferred interpolation information in user-space in order to
55/// implement curves using [`domain`] and [`sample_with`].
56///
57/// The internals are made transparent to give curve authors freedom, but [the provided constructor]
58/// enforces the required invariants, and the methods maintain those invariants.
59///
60/// [the provided constructor]: EvenCore::new
61/// [`domain`]: EvenCore::domain
62/// [`sample_with`]: EvenCore::sample_with
63///
64/// # Example
65/// ```rust
66/// # use bevy_math::curve::*;
67/// # use bevy_math::curve::cores::*;
68/// // Let's make a curve that interpolates evenly spaced samples using either linear interpolation
69/// // or step "interpolation" — i.e. just using the most recent sample as the source of truth.
70/// enum InterpolationMode {
71///     Linear,
72///     Step,
73/// }
74///
75/// // Linear interpolation mode is driven by a trait.
76/// trait LinearInterpolate {
77///     fn lerp(&self, other: &Self, t: f32) -> Self;
78/// }
79///
80/// // Step interpolation just uses an explicit function.
81/// fn step<T: Clone>(first: &T, second: &T, t: f32) -> T {
82///     if t >= 1.0 {
83///         second.clone()
84///     } else {
85///         first.clone()
86///     }
87/// }
88///
89/// // Omitted: Implementing `LinearInterpolate` on relevant types; e.g. `f32`, `Vec3`, and so on.
90///
91/// // The curve itself uses `EvenCore` to hold the evenly-spaced samples, and the `sample_with`
92/// // function will do all the work of interpolating once given a function to do it with.
93/// struct MyCurve<T> {
94///     core: EvenCore<T>,
95///     interpolation_mode: InterpolationMode,
96/// }
97///
98/// impl<T> Curve<T> for MyCurve<T>
99/// where
100///     T: LinearInterpolate + Clone,
101/// {
102///     fn domain(&self) -> Interval {
103///         self.core.domain()
104///     }
105///     
106///     fn sample_unchecked(&self, t: f32) -> T {
107///         // To sample this curve, check the interpolation mode and dispatch accordingly.
108///         match self.interpolation_mode {
109///             InterpolationMode::Linear => self.core.sample_with(t, <T as LinearInterpolate>::lerp),
110///             InterpolationMode::Step => self.core.sample_with(t, step),
111///         }
112///     }
113/// }
114/// ```
115#[derive(Debug, Clone, PartialEq)]
116#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
117#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
118pub struct EvenCore<T> {
119    /// The domain over which the samples are taken, which corresponds to the domain of the curve
120    /// formed by interpolating them.
121    ///
122    /// # Invariants
123    /// This must always be a bounded interval; i.e. its endpoints must be finite.
124    pub domain: Interval,
125
126    /// The samples that are interpolated to extract values.
127    ///
128    /// # Invariants
129    /// This must always have a length of at least 2.
130    pub samples: Vec<T>,
131}
132
133/// An error indicating that an [`EvenCore`] could not be constructed.
134#[derive(Debug, Error, Display)]
135#[display("Could not construct an EvenCore")]
136pub enum EvenCoreError {
137    /// Not enough samples were provided.
138    #[display("Need at least two samples to create an EvenCore, but {samples} were provided")]
139    NotEnoughSamples {
140        /// The number of samples that were provided.
141        samples: usize,
142    },
143
144    /// Unbounded domains are not compatible with `EvenCore`.
145    #[display("Cannot create a EvenCore over an unbounded domain")]
146    UnboundedDomain,
147}
148
149impl<T> EvenCore<T> {
150    /// Create a new [`EvenCore`] from the specified `domain` and `samples`. The samples are
151    /// regarded to be evenly spaced within the given domain interval, so that the outermost
152    /// samples form the boundary of that interval. An error is returned if there are not at
153    /// least 2 samples or if the given domain is unbounded.
154    #[inline]
155    pub fn new(
156        domain: Interval,
157        samples: impl IntoIterator<Item = T>,
158    ) -> Result<Self, EvenCoreError> {
159        let samples: Vec<T> = samples.into_iter().collect();
160        if samples.len() < 2 {
161            return Err(EvenCoreError::NotEnoughSamples {
162                samples: samples.len(),
163            });
164        }
165        if !domain.is_bounded() {
166            return Err(EvenCoreError::UnboundedDomain);
167        }
168
169        Ok(EvenCore { domain, samples })
170    }
171
172    /// The domain of the curve derived from this core.
173    #[inline]
174    pub const fn domain(&self) -> Interval {
175        self.domain
176    }
177
178    /// Obtain a value from the held samples using the given `interpolation` to interpolate
179    /// between adjacent samples.
180    ///
181    /// The interpolation takes two values by reference together with a scalar parameter and
182    /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
183    /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
184    #[inline]
185    pub fn sample_with<I>(&self, t: f32, interpolation: I) -> T
186    where
187        T: Clone,
188        I: Fn(&T, &T, f32) -> T,
189    {
190        match even_interp(self.domain, self.samples.len(), t) {
191            InterpolationDatum::Exact(idx)
192            | InterpolationDatum::LeftTail(idx)
193            | InterpolationDatum::RightTail(idx) => self.samples[idx].clone(),
194            InterpolationDatum::Between(lower_idx, upper_idx, s) => {
195                interpolation(&self.samples[lower_idx], &self.samples[upper_idx], s)
196            }
197        }
198    }
199
200    /// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover
201    /// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can
202    /// be used to interpolate between the two contained values with the given parameter. The other
203    /// variants give additional context about where the value is relative to the family of samples.
204    ///
205    /// [`Between`]: `InterpolationDatum::Between`
206    pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&T> {
207        even_interp(self.domain, self.samples.len(), t).map(|idx| &self.samples[idx])
208    }
209
210    /// Like [`sample_interp`], but the returned values include the sample times. This can be
211    /// useful when sample interpolation is not scale-invariant.
212    ///
213    /// [`sample_interp`]: EvenCore::sample_interp
214    pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> {
215        let segment_len = self.domain.length() / (self.samples.len() - 1) as f32;
216        even_interp(self.domain, self.samples.len(), t).map(|idx| {
217            (
218                self.domain.start() + segment_len * idx as f32,
219                &self.samples[idx],
220            )
221        })
222    }
223}
224
225/// Given a domain and a number of samples taken over that interval, return an [`InterpolationDatum`]
226/// that governs how samples are extracted relative to the stored data.
227///
228/// `domain` must be a bounded interval (i.e. `domain.is_bounded() == true`).
229///
230/// `samples` must be at least 2.
231///
232/// This function will never panic, but it may return invalid indices if its assumptions are violated.
233pub fn even_interp(domain: Interval, samples: usize, t: f32) -> InterpolationDatum<usize> {
234    let subdivs = samples - 1;
235    let step = domain.length() / subdivs as f32;
236    let t_shifted = t - domain.start();
237    let steps_taken = t_shifted / step;
238
239    if steps_taken <= 0.0 {
240        // To the left side of all the samples.
241        InterpolationDatum::LeftTail(0)
242    } else if steps_taken >= subdivs as f32 {
243        // To the right side of all the samples
244        InterpolationDatum::RightTail(samples - 1)
245    } else {
246        let lower_index = steps_taken.floor() as usize;
247        // This upper index is always valid because `steps_taken` is a finite value
248        // strictly less than `samples - 1`, so its floor is at most `samples - 2`
249        let upper_index = lower_index + 1;
250        let s = steps_taken.fract();
251        InterpolationDatum::Between(lower_index, upper_index, s)
252    }
253}
254
255/// The data core of a curve defined by unevenly-spaced samples or keyframes. The intention is to
256/// use this in concert with implicitly or explicitly-defined interpolation in user-space in
257/// order to implement the curve interface using [`domain`] and [`sample_with`].
258///
259/// The internals are made transparent to give curve authors freedom, but [the provided constructor]
260/// enforces the required invariants, and the methods maintain those invariants.
261///
262/// # Example
263/// ```rust
264/// # use bevy_math::curve::*;
265/// # use bevy_math::curve::cores::*;
266/// // Let's make a curve formed by interpolating rotations.
267/// // We'll support two common modes of interpolation:
268/// // - Normalized linear: First do linear interpolation, then normalize to get a valid rotation.
269/// // - Spherical linear: Interpolate through valid rotations with constant angular velocity.
270/// enum InterpolationMode {
271///     NormalizedLinear,
272///     SphericalLinear,
273/// }
274///
275/// // Our interpolation modes will be driven by traits.
276/// trait NormalizedLinearInterpolate {
277///     fn nlerp(&self, other: &Self, t: f32) -> Self;
278/// }
279///
280/// trait SphericalLinearInterpolate {
281///     fn slerp(&self, other: &Self, t: f32) -> Self;
282/// }
283///
284/// // Omitted: These traits would be implemented for `Rot2`, `Quat`, and other rotation representations.
285///
286/// // The curve itself just needs to use the curve core for keyframes, `UnevenCore`, which handles
287/// // everything except for the explicit interpolation used.
288/// struct RotationCurve<T> {
289///     core: UnevenCore<T>,
290///     interpolation_mode: InterpolationMode,
291/// }
292///
293/// impl<T> Curve<T> for RotationCurve<T>
294/// where
295///     T: NormalizedLinearInterpolate + SphericalLinearInterpolate + Clone,
296/// {
297///     fn domain(&self) -> Interval {
298///         self.core.domain()
299///     }
300///     
301///     fn sample_unchecked(&self, t: f32) -> T {
302///         // To sample the curve, we just look at the interpolation mode and
303///         // dispatch accordingly.
304///         match self.interpolation_mode {
305///             InterpolationMode::NormalizedLinear =>
306///                 self.core.sample_with(t, <T as NormalizedLinearInterpolate>::nlerp),
307///             InterpolationMode::SphericalLinear =>
308///                 self.core.sample_with(t, <T as SphericalLinearInterpolate>::slerp),
309///         }
310///     }
311/// }
312/// ```
313///
314/// [`domain`]: UnevenCore::domain
315/// [`sample_with`]: UnevenCore::sample_with
316/// [the provided constructor]: UnevenCore::new
317#[derive(Debug, Clone)]
318#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
319#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
320pub struct UnevenCore<T> {
321    /// The times for the samples of this curve.
322    ///
323    /// # Invariants
324    /// This must always have a length of at least 2, be sorted, and have no
325    /// duplicated or non-finite times.
326    pub times: Vec<f32>,
327
328    /// The samples corresponding to the times for this curve.
329    ///
330    /// # Invariants
331    /// This must always have the same length as `times`.
332    pub samples: Vec<T>,
333}
334
335/// An error indicating that an [`UnevenCore`] could not be constructed.
336#[derive(Debug, Error, Display)]
337#[display("Could not construct an UnevenCore")]
338pub enum UnevenCoreError {
339    /// Not enough samples were provided.
340    #[display(
341        "Need at least two unique samples to create an UnevenCore, but {samples} were provided"
342    )]
343    NotEnoughSamples {
344        /// The number of samples that were provided.
345        samples: usize,
346    },
347}
348
349impl<T> UnevenCore<T> {
350    /// Create a new [`UnevenCore`]. The given samples are filtered to finite times and
351    /// sorted internally; if there are not at least 2 valid timed samples, an error will be
352    /// returned.
353    pub fn new(timed_samples: impl IntoIterator<Item = (f32, T)>) -> Result<Self, UnevenCoreError> {
354        // Filter out non-finite sample times first so they don't interfere with sorting/deduplication.
355        let mut timed_samples = timed_samples
356            .into_iter()
357            .filter(|(t, _)| t.is_finite())
358            .collect_vec();
359        timed_samples
360            // Using `total_cmp` is fine because no NANs remain and because deduplication uses
361            // `PartialEq` anyway (so -0.0 and 0.0 will be considered equal later regardless).
362            .sort_by(|(t0, _), (t1, _)| t0.total_cmp(t1));
363        timed_samples.dedup_by_key(|(t, _)| *t);
364
365        if timed_samples.len() < 2 {
366            return Err(UnevenCoreError::NotEnoughSamples {
367                samples: timed_samples.len(),
368            });
369        }
370
371        let (times, samples): (Vec<f32>, Vec<T>) = timed_samples.into_iter().unzip();
372        Ok(UnevenCore { times, samples })
373    }
374
375    /// The domain of the curve derived from this core.
376    ///
377    /// # Panics
378    /// This method may panic if the type's invariants aren't satisfied.
379    #[inline]
380    pub fn domain(&self) -> Interval {
381        let start = self.times.first().unwrap();
382        let end = self.times.last().unwrap();
383        Interval::new(*start, *end).unwrap()
384    }
385
386    /// Obtain a value from the held samples using the given `interpolation` to interpolate
387    /// between adjacent samples.
388    ///
389    /// The interpolation takes two values by reference together with a scalar parameter and
390    /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
391    /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
392    #[inline]
393    pub fn sample_with<I>(&self, t: f32, interpolation: I) -> T
394    where
395        T: Clone,
396        I: Fn(&T, &T, f32) -> T,
397    {
398        match uneven_interp(&self.times, t) {
399            InterpolationDatum::Exact(idx)
400            | InterpolationDatum::LeftTail(idx)
401            | InterpolationDatum::RightTail(idx) => self.samples[idx].clone(),
402            InterpolationDatum::Between(lower_idx, upper_idx, s) => {
403                interpolation(&self.samples[lower_idx], &self.samples[upper_idx], s)
404            }
405        }
406    }
407
408    /// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover
409    /// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can
410    /// be used to interpolate between the two contained values with the given parameter. The other
411    /// variants give additional context about where the value is relative to the family of samples.
412    ///
413    /// [`Between`]: `InterpolationDatum::Between`
414    pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&T> {
415        uneven_interp(&self.times, t).map(|idx| &self.samples[idx])
416    }
417
418    /// Like [`sample_interp`], but the returned values include the sample times. This can be
419    /// useful when sample interpolation is not scale-invariant.
420    ///
421    /// [`sample_interp`]: UnevenCore::sample_interp
422    pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> {
423        uneven_interp(&self.times, t).map(|idx| (self.times[idx], &self.samples[idx]))
424    }
425
426    /// This core, but with the sample times moved by the map `f`.
427    /// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`],
428    /// but the function inputs to each are inverses of one another.
429    ///
430    /// The samples are re-sorted by time after mapping and deduplicated by output time, so
431    /// the function `f` should generally be injective over the set of sample times, otherwise
432    /// data will be deleted.
433    ///
434    /// [`Curve::reparametrize`]: crate::curve::Curve::reparametrize
435    #[must_use]
436    pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenCore<T> {
437        let mut timed_samples = self
438            .times
439            .into_iter()
440            .map(f)
441            .zip(self.samples)
442            .collect_vec();
443        timed_samples.sort_by(|(t1, _), (t2, _)| t1.total_cmp(t2));
444        timed_samples.dedup_by_key(|(t, _)| *t);
445        (self.times, self.samples) = timed_samples.into_iter().unzip();
446        self
447    }
448}
449
450/// The data core of a curve using uneven samples (i.e. keyframes), where each sample time
451/// yields some fixed number of values — the [sampling width]. This may serve as storage for
452/// curves that yield vectors or iterators, and in some cases, it may be useful for cache locality
453/// if the sample type can effectively be encoded as a fixed-length slice of values.
454///
455/// [sampling width]: ChunkedUnevenCore::width
456#[derive(Debug, Clone)]
457#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
458#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
459pub struct ChunkedUnevenCore<T> {
460    /// The times, one for each sample.
461    ///
462    /// # Invariants
463    /// This must always have a length of at least 2, be sorted, and have no duplicated or
464    /// non-finite times.
465    pub times: Vec<f32>,
466
467    /// The values that are used in sampling. Each width-worth of these correspond to a single sample.
468    ///
469    /// # Invariants
470    /// The length of this vector must always be some fixed integer multiple of that of `times`.
471    pub values: Vec<T>,
472}
473
474/// An error that indicates that a [`ChunkedUnevenCore`] could not be formed.
475#[derive(Debug, Error, Display)]
476#[display("Could not create a ChunkedUnevenCore")]
477pub enum ChunkedUnevenCoreError {
478    /// The width of a `ChunkedUnevenCore` cannot be zero.
479    #[display("Chunk width must be at least 1")]
480    ZeroWidth,
481
482    /// At least two sample times are necessary to interpolate in `ChunkedUnevenCore`.
483    #[display(
484        "Need at least two unique samples to create a ChunkedUnevenCore, but {samples} were provided"
485    )]
486    NotEnoughSamples {
487        /// The number of samples that were provided.
488        samples: usize,
489    },
490
491    /// The length of the value buffer is supposed to be the `width` times the number of samples.
492    #[display("Expected {expected} total values based on width, but {actual} were provided")]
493    MismatchedLengths {
494        /// The expected length of the value buffer.
495        expected: usize,
496        /// The actual length of the value buffer.
497        actual: usize,
498    },
499
500    /// Tried to infer the width, but the ratio of lengths wasn't an integer, so no such length exists.
501    #[display("The length of the list of values ({values_len}) was not divisible by that of the list of times ({times_len})")]
502    NonDivisibleLengths {
503        /// The length of the value buffer.
504        values_len: usize,
505        /// The length of the time buffer.
506        times_len: usize,
507    },
508}
509
510impl<T> ChunkedUnevenCore<T> {
511    /// Create a new [`ChunkedUnevenCore`]. The given `times` are sorted, filtered to finite times,
512    /// and deduplicated. See the [type-level documentation] for more information about this type.
513    ///
514    /// Produces an error in any of the following circumstances:
515    /// - `width` is zero.
516    /// - `times` has less than `2` unique valid entries.
517    /// - `values` has the incorrect length relative to `times`.
518    ///
519    /// [type-level documentation]: ChunkedUnevenCore
520    pub fn new(
521        times: impl IntoIterator<Item = f32>,
522        values: impl IntoIterator<Item = T>,
523        width: usize,
524    ) -> Result<Self, ChunkedUnevenCoreError> {
525        let times = times.into_iter().collect_vec();
526        let values = values.into_iter().collect_vec();
527
528        if width == 0 {
529            return Err(ChunkedUnevenCoreError::ZeroWidth);
530        }
531
532        let times = filter_sort_dedup_times(times);
533
534        if times.len() < 2 {
535            return Err(ChunkedUnevenCoreError::NotEnoughSamples {
536                samples: times.len(),
537            });
538        }
539
540        if values.len() != times.len() * width {
541            return Err(ChunkedUnevenCoreError::MismatchedLengths {
542                expected: times.len() * width,
543                actual: values.len(),
544            });
545        }
546
547        Ok(Self { times, values })
548    }
549
550    /// Create a new [`ChunkedUnevenCore`], inferring the width from the sizes of the inputs.
551    /// The given `times` are sorted, filtered to finite times, and deduplicated. See the
552    /// [type-level documentation] for more information about this type. Prefer using [`new`]
553    /// if possible, since that constructor has richer error checking.
554    ///
555    /// Produces an error in any of the following circumstances:
556    /// - `values` has length zero.
557    /// - `times` has less than `2` unique valid entries.
558    /// - The length of `values` is not divisible by that of `times` (once sorted, filtered,
559    ///   and deduplicated).
560    ///
561    /// The [width] is implicitly taken to be the length of `values` divided by that of `times`
562    /// (once sorted, filtered, and deduplicated).
563    ///
564    /// [type-level documentation]: ChunkedUnevenCore
565    /// [`new`]: ChunkedUnevenCore::new
566    /// [width]: ChunkedUnevenCore::width
567    pub fn new_width_inferred(
568        times: impl IntoIterator<Item = f32>,
569        values: impl IntoIterator<Item = T>,
570    ) -> Result<Self, ChunkedUnevenCoreError> {
571        let times = times.into_iter().collect_vec();
572        let values = values.into_iter().collect_vec();
573
574        let times = filter_sort_dedup_times(times);
575
576        if times.len() < 2 {
577            return Err(ChunkedUnevenCoreError::NotEnoughSamples {
578                samples: times.len(),
579            });
580        }
581
582        if values.len() % times.len() != 0 {
583            return Err(ChunkedUnevenCoreError::NonDivisibleLengths {
584                values_len: values.len(),
585                times_len: times.len(),
586            });
587        }
588
589        if values.is_empty() {
590            return Err(ChunkedUnevenCoreError::ZeroWidth);
591        }
592
593        Ok(Self { times, values })
594    }
595
596    /// The domain of the curve derived from this core.
597    ///
598    /// # Panics
599    /// This may panic if this type's invariants aren't met.
600    #[inline]
601    pub fn domain(&self) -> Interval {
602        let start = self.times.first().unwrap();
603        let end = self.times.last().unwrap();
604        Interval::new(*start, *end).unwrap()
605    }
606
607    /// The sample width: the number of values that are contained in each sample.
608    #[inline]
609    pub fn width(&self) -> usize {
610        self.values.len() / self.times.len()
611    }
612
613    /// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover
614    /// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can
615    /// be used to interpolate between the two contained values with the given parameter. The other
616    /// variants give additional context about where the value is relative to the family of samples.
617    ///
618    /// [`Between`]: `InterpolationDatum::Between`
619    #[inline]
620    pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&[T]> {
621        uneven_interp(&self.times, t).map(|idx| self.time_index_to_slice(idx))
622    }
623
624    /// Like [`sample_interp`], but the returned values include the sample times. This can be
625    /// useful when sample interpolation is not scale-invariant.
626    ///
627    /// [`sample_interp`]: ChunkedUnevenCore::sample_interp
628    pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &[T])> {
629        uneven_interp(&self.times, t).map(|idx| (self.times[idx], self.time_index_to_slice(idx)))
630    }
631
632    /// Given an index in [times], returns the slice of [values] that correspond to the sample at
633    /// that time.
634    ///
635    /// [times]: ChunkedUnevenCore::times
636    /// [values]: ChunkedUnevenCore::values
637    #[inline]
638    fn time_index_to_slice(&self, idx: usize) -> &[T] {
639        let width = self.width();
640        let lower_idx = width * idx;
641        let upper_idx = lower_idx + width;
642        &self.values[lower_idx..upper_idx]
643    }
644}
645
646/// Sort the given times, deduplicate them, and filter them to only finite times.
647fn filter_sort_dedup_times(times: impl IntoIterator<Item = f32>) -> Vec<f32> {
648    // Filter before sorting/deduplication so that NAN doesn't interfere with them.
649    let mut times = times.into_iter().filter(|t| t.is_finite()).collect_vec();
650    times.sort_by(f32::total_cmp);
651    times.dedup();
652    times
653}
654
655/// Given a list of `times` and a target value, get the interpolation relationship for the
656/// target value in terms of the indices of the starting list. In a sense, this encapsulates the
657/// heart of uneven/keyframe sampling.
658///
659/// `times` is assumed to be sorted, deduplicated, and consisting only of finite values. It is also
660/// assumed to contain at least two values.
661///
662/// # Panics
663/// This function will panic if `times` contains NAN.
664pub fn uneven_interp(times: &[f32], t: f32) -> InterpolationDatum<usize> {
665    match times.binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) {
666        Ok(index) => InterpolationDatum::Exact(index),
667        Err(index) => {
668            if index == 0 {
669                // This is before the first keyframe.
670                InterpolationDatum::LeftTail(0)
671            } else if index >= times.len() {
672                // This is after the last keyframe.
673                InterpolationDatum::RightTail(times.len() - 1)
674            } else {
675                // This is actually in the middle somewhere.
676                let t_lower = times[index - 1];
677                let t_upper = times[index];
678                let s = (t - t_lower) / (t_upper - t_lower);
679                InterpolationDatum::Between(index - 1, index, s)
680            }
681        }
682    }
683}
684
685#[cfg(test)]
686mod tests {
687    use super::{ChunkedUnevenCore, EvenCore, UnevenCore};
688    use crate::curve::{cores::InterpolationDatum, interval};
689    // use approx::{assert_abs_diff_eq, AbsDiffEq};
690
691    // fn approx_between<T>(datum: InterpolationDatum<T>, start: T, end: T, p: f32) -> bool
692    // where
693    //     T: PartialEq,
694    // {
695    //     if let InterpolationDatum::Between(m_start, m_end, m_p) = datum {
696    //         m_start == start && m_end == end && m_p.abs_diff_eq(&p, 1e-6)
697    //     } else {
698    //         false
699    //     }
700    // }
701
702    fn is_left_tail<T>(datum: InterpolationDatum<T>) -> bool {
703        matches!(datum, InterpolationDatum::LeftTail(_))
704    }
705
706    fn is_right_tail<T>(datum: InterpolationDatum<T>) -> bool {
707        matches!(datum, InterpolationDatum::RightTail(_))
708    }
709
710    fn is_exact<T>(datum: InterpolationDatum<T>, target: T) -> bool
711    where
712        T: PartialEq,
713    {
714        if let InterpolationDatum::Exact(v) = datum {
715            v == target
716        } else {
717            false
718        }
719    }
720
721    #[test]
722    fn even_sample_interp() {
723        let even_core = EvenCore::<f32>::new(
724            interval(0.0, 1.0).unwrap(),
725            // 11 entries -> 10 segments
726            vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0],
727        )
728        .expect("Failed to construct test core");
729
730        let datum = even_core.sample_interp(-1.0);
731        assert!(is_left_tail(datum));
732        let datum = even_core.sample_interp(0.0);
733        assert!(is_left_tail(datum));
734        let datum = even_core.sample_interp(1.0);
735        assert!(is_right_tail(datum));
736        let datum = even_core.sample_interp(2.0);
737        assert!(is_right_tail(datum));
738
739        let datum = even_core.sample_interp(0.05);
740        let InterpolationDatum::Between(0.0, 1.0, p) = datum else {
741            panic!("Sample did not lie in the correct subinterval")
742        };
743        // assert_abs_diff_eq!(p, 0.5);
744
745        let datum = even_core.sample_interp(0.05);
746        // assert!(approx_between(datum, &0.0, &1.0, 0.5));
747        let datum = even_core.sample_interp(0.33);
748        // assert!(approx_between(datum, &3.0, &4.0, 0.3));
749        let datum = even_core.sample_interp(0.78);
750        // assert!(approx_between(datum, &7.0, &8.0, 0.8));
751
752        let datum = even_core.sample_interp(0.5);
753        // assert!(approx_between(datum, &4.0, &5.0, 1.0) || approx_between(datum, &5.0, &6.0, 0.0));
754        let datum = even_core.sample_interp(0.7);
755        // assert!(approx_between(datum, &6.0, &7.0, 1.0) || approx_between(datum, &7.0, &8.0, 0.0));
756    }
757
758    #[test]
759    fn uneven_sample_interp() {
760        let uneven_core = UnevenCore::<f32>::new(vec![
761            (0.0, 0.0),
762            (1.0, 3.0),
763            (2.0, 9.0),
764            (4.0, 10.0),
765            (8.0, -5.0),
766        ])
767        .expect("Failed to construct test core");
768
769        let datum = uneven_core.sample_interp(-1.0);
770        assert!(is_left_tail(datum));
771        let datum = uneven_core.sample_interp(0.0);
772        assert!(is_exact(datum, &0.0));
773        let datum = uneven_core.sample_interp(8.0);
774        assert!(is_exact(datum, &(-5.0)));
775        let datum = uneven_core.sample_interp(9.0);
776        assert!(is_right_tail(datum));
777
778        let datum = uneven_core.sample_interp(0.5);
779        // assert!(approx_between(datum, &0.0, &3.0, 0.5));
780        let datum = uneven_core.sample_interp(2.5);
781        // assert!(approx_between(datum, &9.0, &10.0, 0.25));
782        let datum = uneven_core.sample_interp(7.0);
783        // assert!(approx_between(datum, &10.0, &(-5.0), 0.75));
784
785        let datum = uneven_core.sample_interp(2.0);
786        assert!(is_exact(datum, &9.0));
787        let datum = uneven_core.sample_interp(4.0);
788        assert!(is_exact(datum, &10.0));
789    }
790
791    #[test]
792    fn chunked_uneven_sample_interp() {
793        let core =
794            ChunkedUnevenCore::new(vec![0.0, 2.0, 8.0], vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0], 2)
795                .expect("Failed to construct test core");
796
797        let datum = core.sample_interp(-1.0);
798        assert!(is_left_tail(datum));
799        let datum = core.sample_interp(0.0);
800        assert!(is_exact(datum, &[0.0, 1.0]));
801        let datum = core.sample_interp(8.0);
802        assert!(is_exact(datum, &[4.0, 5.0]));
803        let datum = core.sample_interp(10.0);
804        assert!(is_right_tail(datum));
805
806        let datum = core.sample_interp(1.0);
807        // assert!(approx_between(datum, &[0.0, 1.0], &[2.0, 3.0], 0.5));
808        let datum = core.sample_interp(3.0);
809        // assert!(approx_between(datum, &[2.0, 3.0], &[4.0, 5.0], 1.0 / 6.0));
810
811        let datum = core.sample_interp(2.0);
812        assert!(is_exact(datum, &[2.0, 3.0]));
813    }
814}