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