A Curve trait for general interoperation — Part II (#14700)
# Objective Finish what we started in #14630. The Curve RFC is [here](https://github.com/bevyengine/rfcs/blob/main/rfcs/80-curve-trait.md). ## Solution This contains the rest of the library from my branch. The main things added here are: - Bulk sampling / resampling methods on `Curve` itself - Data structures supporting the above - The `cores` submodule that those data structures use to encapsulate sample interpolation The weirdest thing in here is probably `ChunkedUnevenCore` in `cores`, which is not used by anything in the Curve library itself but which is required for efficient storage of glTF animation curves. (See #13105.) We can move it into a different PR if we want to; I don't have strong feelings either way. ## Testing New tests related to resampling are included. As I write this, I realize we could use some tests in `cores` itself, so I will add some on this branch before too long. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Robert Walter <26892280+RobWalt@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									2012f13c05
								
							
						
					
					
						commit
						20a9b921a0
					
				
							
								
								
									
										628
									
								
								crates/bevy_math/src/curve/cores.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										628
									
								
								crates/bevy_math/src/curve/cores.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,628 @@ | ||||
| //! Core data structures to be used internally in Curve implementations, encapsulating storage
 | ||||
| //! and access patterns for reuse.
 | ||||
| //!
 | ||||
| //! The `Core` types here expose their fields publicly so that it is easier to manipulate and
 | ||||
| //! extend them, but in doing so, you must maintain the invariants of those fields yourself. The
 | ||||
| //! provided methods all maintain the invariants, so this is only a concern if you manually mutate
 | ||||
| //! the fields.
 | ||||
| 
 | ||||
| use super::interval::Interval; | ||||
| use core::fmt::Debug; | ||||
| use itertools::Itertools; | ||||
| use thiserror::Error; | ||||
| 
 | ||||
| #[cfg(feature = "bevy_reflect")] | ||||
| use bevy_reflect::Reflect; | ||||
| 
 | ||||
| /// This type expresses the relationship of a value to a fixed collection of values. It is a kind
 | ||||
| /// of summary used intermediately by sampling operations.
 | ||||
| #[derive(Debug, Copy, Clone, PartialEq)] | ||||
| #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] | ||||
| #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] | ||||
| pub enum InterpolationDatum<T> { | ||||
|     /// This value lies exactly on a value in the family.
 | ||||
|     Exact(T), | ||||
| 
 | ||||
|     /// This value is off the left tail of the family; the inner value is the family's leftmost.
 | ||||
|     LeftTail(T), | ||||
| 
 | ||||
|     /// This value is off the right tail of the family; the inner value is the family's rightmost.
 | ||||
|     RightTail(T), | ||||
| 
 | ||||
|     /// This value lies on the interior, in between two points, with a third parameter expressing
 | ||||
|     /// the interpolation factor between the two.
 | ||||
|     Between(T, T, f32), | ||||
| } | ||||
| 
 | ||||
| impl<T> InterpolationDatum<T> { | ||||
|     /// Map all values using a given function `f`, leaving the interpolation parameters in any
 | ||||
|     /// [`Between`] variants unchanged.
 | ||||
|     ///
 | ||||
|     /// [`Between`]: `InterpolationDatum::Between`
 | ||||
|     #[must_use] | ||||
|     pub fn map<S>(self, f: impl Fn(T) -> S) -> InterpolationDatum<S> { | ||||
|         match self { | ||||
|             InterpolationDatum::Exact(v) => InterpolationDatum::Exact(f(v)), | ||||
|             InterpolationDatum::LeftTail(v) => InterpolationDatum::LeftTail(f(v)), | ||||
|             InterpolationDatum::RightTail(v) => InterpolationDatum::RightTail(f(v)), | ||||
|             InterpolationDatum::Between(u, v, s) => InterpolationDatum::Between(f(u), f(v), s), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// The data core of a curve derived from evenly-spaced samples. The intention is to use this
 | ||||
| /// in addition to explicit or inferred interpolation information in user-space in order to
 | ||||
| /// implement curves using [`domain`] and [`sample_with`].
 | ||||
| ///
 | ||||
| /// The internals are made transparent to give curve authors freedom, but [the provided constructor]
 | ||||
| /// enforces the required invariants, and the methods maintain those invariants.
 | ||||
| ///
 | ||||
| /// [the provided constructor]: EvenCore::new
 | ||||
| /// [`domain`]: EvenCore::domain
 | ||||
| /// [`sample_with`]: EvenCore::sample_with
 | ||||
| ///
 | ||||
| /// # Example
 | ||||
| /// ```rust
 | ||||
| /// # use bevy_math::curve::*;
 | ||||
| /// # use bevy_math::curve::cores::*;
 | ||||
| /// // Let's make a curve that interpolates evenly spaced samples using either linear interpolation
 | ||||
| /// // or step "interpolation" — i.e. just using the most recent sample as the source of truth.
 | ||||
| /// enum InterpolationMode {
 | ||||
| ///     Linear,
 | ||||
| ///     Step,
 | ||||
| /// }
 | ||||
| ///
 | ||||
| /// // Linear interpolation mode is driven by a trait.
 | ||||
| /// trait LinearInterpolate {
 | ||||
| ///     fn lerp(&self, other: &Self, t: f32) -> Self;
 | ||||
| /// }
 | ||||
| ///
 | ||||
| /// // Step interpolation just uses an explicit function.
 | ||||
| /// fn step<T: Clone>(first: &T, second: &T, t: f32) -> T {
 | ||||
| ///     if t >= 1.0 {
 | ||||
| ///         second.clone()
 | ||||
| ///     } else {
 | ||||
| ///         first.clone()
 | ||||
| ///     }
 | ||||
| /// }
 | ||||
| ///
 | ||||
| /// // Omitted: Implementing `LinearInterpolate` on relevant types; e.g. `f32`, `Vec3`, and so on.
 | ||||
| ///
 | ||||
| /// // The curve itself uses `EvenCore` to hold the evenly-spaced samples, and the `sample_with`
 | ||||
| /// // function will do all the work of interpolating once given a function to do it with.
 | ||||
| /// struct MyCurve<T> {
 | ||||
| ///     core: EvenCore<T>,
 | ||||
| ///     interpolation_mode: InterpolationMode,
 | ||||
| /// }
 | ||||
| ///
 | ||||
| /// impl<T> Curve<T> for MyCurve<T>
 | ||||
| /// where
 | ||||
| ///     T: LinearInterpolate + Clone,
 | ||||
| /// {
 | ||||
| ///     fn domain(&self) -> Interval {
 | ||||
| ///         self.core.domain()
 | ||||
| ///     }
 | ||||
| ///     
 | ||||
| ///     fn sample_unchecked(&self, t: f32) -> T {
 | ||||
| ///         // To sample this curve, check the interpolation mode and dispatch accordingly.
 | ||||
| ///         match self.interpolation_mode {
 | ||||
| ///             InterpolationMode::Linear => self.core.sample_with(t, <T as LinearInterpolate>::lerp),
 | ||||
| ///             InterpolationMode::Step => self.core.sample_with(t, step),
 | ||||
| ///         }
 | ||||
| ///     }
 | ||||
| /// }
 | ||||
| /// ```
 | ||||
| #[derive(Debug, Clone, PartialEq)] | ||||
| #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] | ||||
| #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] | ||||
| pub struct EvenCore<T> { | ||||
|     /// The domain over which the samples are taken, which corresponds to the domain of the curve
 | ||||
|     /// formed by interpolating them.
 | ||||
|     ///
 | ||||
|     /// # Invariants
 | ||||
|     /// This must always be a bounded interval; i.e. its endpoints must be finite.
 | ||||
|     pub domain: Interval, | ||||
| 
 | ||||
|     /// The samples that are interpolated to extract values.
 | ||||
|     ///
 | ||||
|     /// # Invariants
 | ||||
|     /// This must always have a length of at least 2.
 | ||||
|     pub samples: Vec<T>, | ||||
| } | ||||
| 
 | ||||
| /// An error indicating that an [`EvenCore`] could not be constructed.
 | ||||
| #[derive(Debug, Error)] | ||||
| #[error("Could not construct an EvenCore")] | ||||
| pub enum EvenCoreError { | ||||
|     /// Not enough samples were provided.
 | ||||
|     #[error("Need at least two samples to create an EvenCore, but {samples} were provided")] | ||||
|     NotEnoughSamples { | ||||
|         /// The number of samples that were provided.
 | ||||
|         samples: usize, | ||||
|     }, | ||||
| 
 | ||||
|     /// Unbounded domains are not compatible with `EvenCore`.
 | ||||
|     #[error("Cannot create a EvenCore over an unbounded domain")] | ||||
|     UnboundedDomain, | ||||
| } | ||||
| 
 | ||||
| impl<T> EvenCore<T> { | ||||
|     /// Create a new [`EvenCore`] from the specified `domain` and `samples`. The samples are
 | ||||
|     /// regarded to be evenly spaced within the given domain interval, so that the outermost
 | ||||
|     /// samples form the boundary of that interval. An error is returned if there are not at
 | ||||
|     /// least 2 samples or if the given domain is unbounded.
 | ||||
|     #[inline] | ||||
|     pub fn new( | ||||
|         domain: Interval, | ||||
|         samples: impl IntoIterator<Item = T>, | ||||
|     ) -> Result<Self, EvenCoreError> { | ||||
|         let samples: Vec<T> = samples.into_iter().collect(); | ||||
|         if samples.len() < 2 { | ||||
|             return Err(EvenCoreError::NotEnoughSamples { | ||||
|                 samples: samples.len(), | ||||
|             }); | ||||
|         } | ||||
|         if !domain.is_bounded() { | ||||
|             return Err(EvenCoreError::UnboundedDomain); | ||||
|         } | ||||
| 
 | ||||
|         Ok(EvenCore { domain, samples }) | ||||
|     } | ||||
| 
 | ||||
|     /// The domain of the curve derived from this core.
 | ||||
|     #[inline] | ||||
|     pub fn domain(&self) -> Interval { | ||||
|         self.domain | ||||
|     } | ||||
| 
 | ||||
|     /// Obtain a value from the held samples using the given `interpolation` to interpolate
 | ||||
|     /// between adjacent samples.
 | ||||
|     ///
 | ||||
|     /// The interpolation takes two values by reference together with a scalar parameter and
 | ||||
|     /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
 | ||||
|     /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
 | ||||
|     #[inline] | ||||
|     pub fn sample_with<I>(&self, t: f32, interpolation: I) -> T | ||||
|     where | ||||
|         T: Clone, | ||||
|         I: Fn(&T, &T, f32) -> T, | ||||
|     { | ||||
|         match even_interp(self.domain, self.samples.len(), t) { | ||||
|             InterpolationDatum::Exact(idx) | ||||
|             | InterpolationDatum::LeftTail(idx) | ||||
|             | InterpolationDatum::RightTail(idx) => self.samples[idx].clone(), | ||||
|             InterpolationDatum::Between(lower_idx, upper_idx, s) => { | ||||
|                 interpolation(&self.samples[lower_idx], &self.samples[upper_idx], s) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover
 | ||||
|     /// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can
 | ||||
|     /// be used to interpolate between the two contained values with the given parameter. The other
 | ||||
|     /// variants give additional context about where the value is relative to the family of samples.
 | ||||
|     ///
 | ||||
|     /// [`Between`]: `InterpolationDatum::Between`
 | ||||
|     pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&T> { | ||||
|         even_interp(self.domain, self.samples.len(), t).map(|idx| &self.samples[idx]) | ||||
|     } | ||||
| 
 | ||||
|     /// Like [`sample_interp`], but the returned values include the sample times. This can be
 | ||||
|     /// useful when sample interpolation is not scale-invariant.
 | ||||
|     ///
 | ||||
|     /// [`sample_interp`]: EvenCore::sample_interp
 | ||||
|     pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> { | ||||
|         let segment_len = self.domain.length() / (self.samples.len() - 1) as f32; | ||||
|         even_interp(self.domain, self.samples.len(), t).map(|idx| { | ||||
|             ( | ||||
|                 self.domain.start() + segment_len * idx as f32, | ||||
|                 &self.samples[idx], | ||||
|             ) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Given a domain and a number of samples taken over that interval, return an [`InterpolationDatum`]
 | ||||
| /// that governs how samples are extracted relative to the stored data.
 | ||||
| ///
 | ||||
| /// `domain` must be a bounded interval (i.e. `domain.is_bounded() == true`).
 | ||||
| ///
 | ||||
| /// `samples` must be at least 2.
 | ||||
| ///
 | ||||
| /// This function will never panic, but it may return invalid indices if its assumptions are violated.
 | ||||
| pub fn even_interp(domain: Interval, samples: usize, t: f32) -> InterpolationDatum<usize> { | ||||
|     let subdivs = samples - 1; | ||||
|     let step = domain.length() / subdivs as f32; | ||||
|     let t_shifted = t - domain.start(); | ||||
|     let steps_taken = t_shifted / step; | ||||
| 
 | ||||
|     if steps_taken <= 0.0 { | ||||
|         // To the left side of all the samples.
 | ||||
|         InterpolationDatum::LeftTail(0) | ||||
|     } else if steps_taken >= subdivs as f32 { | ||||
|         // To the right side of all the samples
 | ||||
|         InterpolationDatum::RightTail(samples - 1) | ||||
|     } else { | ||||
|         let lower_index = steps_taken.floor() as usize; | ||||
|         // This upper index is always valid because `steps_taken` is a finite value
 | ||||
|         // strictly less than `samples - 1`, so its floor is at most `samples - 2`
 | ||||
|         let upper_index = lower_index + 1; | ||||
|         let s = steps_taken.fract(); | ||||
|         InterpolationDatum::Between(lower_index, upper_index, s) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// The data core of a curve defined by unevenly-spaced samples or keyframes. The intention is to
 | ||||
| /// use this in concert with implicitly or explicitly-defined interpolation in user-space in
 | ||||
| /// order to implement the curve interface using [`domain`] and [`sample_with`].
 | ||||
| ///
 | ||||
| /// The internals are made transparent to give curve authors freedom, but [the provided constructor]
 | ||||
| /// enforces the required invariants, and the methods maintain those invariants.
 | ||||
| ///
 | ||||
| /// # Example
 | ||||
| /// ```rust
 | ||||
| /// # use bevy_math::curve::*;
 | ||||
| /// # use bevy_math::curve::cores::*;
 | ||||
| /// // Let's make a curve formed by interpolating rotations.
 | ||||
| /// // We'll support two common modes of interpolation:
 | ||||
| /// // - Normalized linear: First do linear interpolation, then normalize to get a valid rotation.
 | ||||
| /// // - Spherical linear: Interpolate through valid rotations with constant angular velocity.
 | ||||
| /// enum InterpolationMode {
 | ||||
| ///     NormalizedLinear,
 | ||||
| ///     SphericalLinear,
 | ||||
| /// }
 | ||||
| ///
 | ||||
| /// // Our interpolation modes will be driven by traits.
 | ||||
| /// trait NormalizedLinearInterpolate {
 | ||||
| ///     fn nlerp(&self, other: &Self, t: f32) -> Self;
 | ||||
| /// }
 | ||||
| ///
 | ||||
| /// trait SphericalLinearInterpolate {
 | ||||
| ///     fn slerp(&self, other: &Self, t: f32) -> Self;
 | ||||
| /// }
 | ||||
| ///
 | ||||
| /// // Omitted: These traits would be implemented for `Rot2`, `Quat`, and other rotation representations.
 | ||||
| ///
 | ||||
| /// // The curve itself just needs to use the curve core for keyframes, `UnevenCore`, which handles
 | ||||
| /// // everything except for the explicit interpolation used.
 | ||||
| /// struct RotationCurve<T> {
 | ||||
| ///     core: UnevenCore<T>,
 | ||||
| ///     interpolation_mode: InterpolationMode,
 | ||||
| /// }
 | ||||
| ///
 | ||||
| /// impl<T> Curve<T> for RotationCurve<T>
 | ||||
| /// where
 | ||||
| ///     T: NormalizedLinearInterpolate + SphericalLinearInterpolate + Clone,
 | ||||
| /// {
 | ||||
| ///     fn domain(&self) -> Interval {
 | ||||
| ///         self.core.domain()
 | ||||
| ///     }
 | ||||
| ///     
 | ||||
| ///     fn sample_unchecked(&self, t: f32) -> T {
 | ||||
| ///         // To sample the curve, we just look at the interpolation mode and
 | ||||
| ///         // dispatch accordingly.
 | ||||
| ///         match self.interpolation_mode {
 | ||||
| ///             InterpolationMode::NormalizedLinear =>
 | ||||
| ///                 self.core.sample_with(t, <T as NormalizedLinearInterpolate>::nlerp),
 | ||||
| ///             InterpolationMode::SphericalLinear =>
 | ||||
| ///                 self.core.sample_with(t, <T as SphericalLinearInterpolate>::slerp),
 | ||||
| ///         }
 | ||||
| ///     }
 | ||||
| /// }
 | ||||
| /// ```
 | ||||
| ///
 | ||||
| /// [`domain`]: UnevenCore::domain
 | ||||
| /// [`sample_with`]: UnevenCore::sample_with
 | ||||
| /// [the provided constructor]: UnevenCore::new
 | ||||
| #[derive(Debug, Clone)] | ||||
| #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] | ||||
| #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] | ||||
| pub struct UnevenCore<T> { | ||||
|     /// The times for the samples of this curve.
 | ||||
|     ///
 | ||||
|     /// # Invariants
 | ||||
|     /// This must always have a length of at least 2, be sorted, and have no
 | ||||
|     /// duplicated or non-finite times.
 | ||||
|     pub times: Vec<f32>, | ||||
| 
 | ||||
|     /// The samples corresponding to the times for this curve.
 | ||||
|     ///
 | ||||
|     /// # Invariants
 | ||||
|     /// This must always have the same length as `times`.
 | ||||
|     pub samples: Vec<T>, | ||||
| } | ||||
| 
 | ||||
| /// An error indicating that an [`UnevenCore`] could not be constructed.
 | ||||
| #[derive(Debug, Error)] | ||||
| #[error("Could not construct an UnevenCore")] | ||||
| pub enum UnevenCoreError { | ||||
|     /// Not enough samples were provided.
 | ||||
|     #[error(
 | ||||
|         "Need at least two unique samples to create an UnevenCore, but {samples} were provided" | ||||
|     )] | ||||
|     NotEnoughSamples { | ||||
|         /// The number of samples that were provided.
 | ||||
|         samples: usize, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| impl<T> UnevenCore<T> { | ||||
|     /// Create a new [`UnevenCore`]. The given samples are filtered to finite times and
 | ||||
|     /// sorted internally; if there are not at least 2 valid timed samples, an error will be
 | ||||
|     /// returned.
 | ||||
|     pub fn new(timed_samples: impl IntoIterator<Item = (f32, T)>) -> Result<Self, UnevenCoreError> { | ||||
|         // Filter out non-finite sample times first so they don't interfere with sorting/deduplication.
 | ||||
|         let mut timed_samples = timed_samples | ||||
|             .into_iter() | ||||
|             .filter(|(t, _)| t.is_finite()) | ||||
|             .collect_vec(); | ||||
|         timed_samples | ||||
|             // Using `total_cmp` is fine because no NANs remain and because deduplication uses
 | ||||
|             // `PartialEq` anyway (so -0.0 and 0.0 will be considered equal later regardless).
 | ||||
|             .sort_by(|(t0, _), (t1, _)| t0.total_cmp(t1)); | ||||
|         timed_samples.dedup_by_key(|(t, _)| *t); | ||||
| 
 | ||||
|         if timed_samples.len() < 2 { | ||||
|             return Err(UnevenCoreError::NotEnoughSamples { | ||||
|                 samples: timed_samples.len(), | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         let (times, samples): (Vec<f32>, Vec<T>) = timed_samples.into_iter().unzip(); | ||||
|         Ok(UnevenCore { times, samples }) | ||||
|     } | ||||
| 
 | ||||
|     /// The domain of the curve derived from this core.
 | ||||
|     ///
 | ||||
|     /// # Panics
 | ||||
|     /// This method may panic if the type's invariants aren't satisfied.
 | ||||
|     #[inline] | ||||
|     pub fn domain(&self) -> Interval { | ||||
|         let start = self.times.first().unwrap(); | ||||
|         let end = self.times.last().unwrap(); | ||||
|         Interval::new(*start, *end).unwrap() | ||||
|     } | ||||
| 
 | ||||
|     /// Obtain a value from the held samples using the given `interpolation` to interpolate
 | ||||
|     /// between adjacent samples.
 | ||||
|     ///
 | ||||
|     /// The interpolation takes two values by reference together with a scalar parameter and
 | ||||
|     /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
 | ||||
|     /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
 | ||||
|     #[inline] | ||||
|     pub fn sample_with<I>(&self, t: f32, interpolation: I) -> T | ||||
|     where | ||||
|         T: Clone, | ||||
|         I: Fn(&T, &T, f32) -> T, | ||||
|     { | ||||
|         match uneven_interp(&self.times, t) { | ||||
|             InterpolationDatum::Exact(idx) | ||||
|             | InterpolationDatum::LeftTail(idx) | ||||
|             | InterpolationDatum::RightTail(idx) => self.samples[idx].clone(), | ||||
|             InterpolationDatum::Between(lower_idx, upper_idx, s) => { | ||||
|                 interpolation(&self.samples[lower_idx], &self.samples[upper_idx], s) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover
 | ||||
|     /// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can
 | ||||
|     /// be used to interpolate between the two contained values with the given parameter. The other
 | ||||
|     /// variants give additional context about where the value is relative to the family of samples.
 | ||||
|     ///
 | ||||
|     /// [`Between`]: `InterpolationDatum::Between`
 | ||||
|     pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&T> { | ||||
|         uneven_interp(&self.times, t).map(|idx| &self.samples[idx]) | ||||
|     } | ||||
| 
 | ||||
|     /// Like [`sample_interp`], but the returned values include the sample times. This can be
 | ||||
|     /// useful when sample interpolation is not scale-invariant.
 | ||||
|     ///
 | ||||
|     /// [`sample_interp`]: UnevenCore::sample_interp
 | ||||
|     pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> { | ||||
|         uneven_interp(&self.times, t).map(|idx| (self.times[idx], &self.samples[idx])) | ||||
|     } | ||||
| 
 | ||||
|     /// This core, but with the sample times moved by the map `f`.
 | ||||
|     /// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`],
 | ||||
|     /// but the function inputs to each are inverses of one another.
 | ||||
|     ///
 | ||||
|     /// The samples are re-sorted by time after mapping and deduplicated by output time, so
 | ||||
|     /// the function `f` should generally be injective over the set of sample times, otherwise
 | ||||
|     /// data will be deleted.
 | ||||
|     ///
 | ||||
|     /// [`Curve::reparametrize`]: crate::curve::Curve::reparametrize
 | ||||
|     #[must_use] | ||||
|     pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenCore<T> { | ||||
|         let mut timed_samples = self | ||||
|             .times | ||||
|             .into_iter() | ||||
|             .map(f) | ||||
|             .zip(self.samples) | ||||
|             .collect_vec(); | ||||
|         timed_samples.sort_by(|(t1, _), (t2, _)| t1.total_cmp(t2)); | ||||
|         timed_samples.dedup_by_key(|(t, _)| *t); | ||||
|         (self.times, self.samples) = timed_samples.into_iter().unzip(); | ||||
|         self | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// The data core of a curve using uneven samples (i.e. keyframes), where each sample time
 | ||||
| /// yields some fixed number of values — the [sampling width]. This may serve as storage for
 | ||||
| /// curves that yield vectors or iterators, and in some cases, it may be useful for cache locality
 | ||||
| /// if the sample type can effectively be encoded as a fixed-length slice of values.
 | ||||
| ///
 | ||||
| /// [sampling width]: ChunkedUnevenCore::width
 | ||||
| #[derive(Debug, Clone)] | ||||
| #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] | ||||
| #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] | ||||
| pub struct ChunkedUnevenCore<T> { | ||||
|     /// The times, one for each sample.
 | ||||
|     ///
 | ||||
|     /// # Invariants
 | ||||
|     /// This must always have a length of at least 2, be sorted, and have no duplicated or
 | ||||
|     /// non-finite times.
 | ||||
|     pub times: Vec<f32>, | ||||
| 
 | ||||
|     /// The values that are used in sampling. Each width-worth of these correspond to a single sample.
 | ||||
|     ///
 | ||||
|     /// # Invariants
 | ||||
|     /// The length of this vector must always be some fixed integer multiple of that of `times`.
 | ||||
|     pub values: Vec<T>, | ||||
| } | ||||
| 
 | ||||
| /// An error that indicates that a [`ChunkedUnevenCore`] could not be formed.
 | ||||
| #[derive(Debug, Error)] | ||||
| #[error("Could not create a ChunkedUnevenCore")] | ||||
| pub enum ChunkedUnevenCoreError { | ||||
|     /// The width of a `ChunkedUnevenCore` cannot be zero.
 | ||||
|     #[error("Chunk width must be at least 1")] | ||||
|     ZeroWidth, | ||||
| 
 | ||||
|     /// At least two sample times are necessary to interpolate in `ChunkedUnevenCore`.
 | ||||
|     #[error(
 | ||||
|         "Need at least two unique samples to create a ChunkedUnevenCore, but {samples} were provided" | ||||
|     )] | ||||
|     NotEnoughSamples { | ||||
|         /// The number of samples that were provided.
 | ||||
|         samples: usize, | ||||
|     }, | ||||
| 
 | ||||
|     /// The length of the value buffer is supposed to be the `width` times the number of samples.
 | ||||
|     #[error("Expected {expected} total values based on width, but {actual} were provided")] | ||||
|     MismatchedLengths { | ||||
|         /// The expected length of the value buffer.
 | ||||
|         expected: usize, | ||||
|         /// The actual length of the value buffer.
 | ||||
|         actual: usize, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| impl<T> ChunkedUnevenCore<T> { | ||||
|     /// Create a new [`ChunkedUnevenCore`]. The given `times` are sorted, filtered to finite times,
 | ||||
|     /// and deduplicated. See the [type-level documentation] for more information about this type.
 | ||||
|     ///
 | ||||
|     /// Produces an error in any of the following circumstances:
 | ||||
|     /// - `width` is zero.
 | ||||
|     /// - `times` has less than `2` valid unique entries.
 | ||||
|     /// - `values` has the incorrect length relative to `times`.
 | ||||
|     ///
 | ||||
|     /// [type-level documentation]: ChunkedUnevenCore
 | ||||
|     pub fn new( | ||||
|         times: impl Into<Vec<f32>>, | ||||
|         values: impl Into<Vec<T>>, | ||||
|         width: usize, | ||||
|     ) -> Result<Self, ChunkedUnevenCoreError> { | ||||
|         let times: Vec<f32> = times.into(); | ||||
|         let values: Vec<T> = values.into(); | ||||
| 
 | ||||
|         if width == 0 { | ||||
|             return Err(ChunkedUnevenCoreError::ZeroWidth); | ||||
|         } | ||||
| 
 | ||||
|         let times = filter_sort_dedup_times(times); | ||||
| 
 | ||||
|         if times.len() < 2 { | ||||
|             return Err(ChunkedUnevenCoreError::NotEnoughSamples { | ||||
|                 samples: times.len(), | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         if values.len() != times.len() * width { | ||||
|             return Err(ChunkedUnevenCoreError::MismatchedLengths { | ||||
|                 expected: times.len() * width, | ||||
|                 actual: values.len(), | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         Ok(Self { times, values }) | ||||
|     } | ||||
| 
 | ||||
|     /// The domain of the curve derived from this core.
 | ||||
|     ///
 | ||||
|     /// # Panics
 | ||||
|     /// This may panic if this type's invariants aren't met.
 | ||||
|     #[inline] | ||||
|     pub fn domain(&self) -> Interval { | ||||
|         let start = self.times.first().unwrap(); | ||||
|         let end = self.times.last().unwrap(); | ||||
|         Interval::new(*start, *end).unwrap() | ||||
|     } | ||||
| 
 | ||||
|     /// The sample width: the number of values that are contained in each sample.
 | ||||
|     #[inline] | ||||
|     pub fn width(&self) -> usize { | ||||
|         self.values.len() / self.times.len() | ||||
|     } | ||||
| 
 | ||||
|     /// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover
 | ||||
|     /// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can
 | ||||
|     /// be used to interpolate between the two contained values with the given parameter. The other
 | ||||
|     /// variants give additional context about where the value is relative to the family of samples.
 | ||||
|     ///
 | ||||
|     /// [`Between`]: `InterpolationDatum::Between`
 | ||||
|     #[inline] | ||||
|     pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&[T]> { | ||||
|         uneven_interp(&self.times, t).map(|idx| self.time_index_to_slice(idx)) | ||||
|     } | ||||
| 
 | ||||
|     /// Like [`sample_interp`], but the returned values include the sample times. This can be
 | ||||
|     /// useful when sample interpolation is not scale-invariant.
 | ||||
|     ///
 | ||||
|     /// [`sample_interp`]: ChunkedUnevenCore::sample_interp
 | ||||
|     pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &[T])> { | ||||
|         uneven_interp(&self.times, t).map(|idx| (self.times[idx], self.time_index_to_slice(idx))) | ||||
|     } | ||||
| 
 | ||||
|     /// Given an index in [times], returns the slice of [values] that correspond to the sample at
 | ||||
|     /// that time.
 | ||||
|     ///
 | ||||
|     /// [times]: ChunkedUnevenCore::times
 | ||||
|     /// [values]: ChunkedUnevenCore::values
 | ||||
|     #[inline] | ||||
|     fn time_index_to_slice(&self, idx: usize) -> &[T] { | ||||
|         let width = self.width(); | ||||
|         let lower_idx = width * idx; | ||||
|         let upper_idx = lower_idx + width; | ||||
|         &self.values[lower_idx..upper_idx] | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Sort the given times, deduplicate them, and filter them to only finite times.
 | ||||
| fn filter_sort_dedup_times(times: impl IntoIterator<Item = f32>) -> Vec<f32> { | ||||
|     // Filter before sorting/deduplication so that NAN doesn't interfere with them.
 | ||||
|     let mut times = times.into_iter().filter(|t| t.is_finite()).collect_vec(); | ||||
|     times.sort_by(f32::total_cmp); | ||||
|     times.dedup(); | ||||
|     times | ||||
| } | ||||
| 
 | ||||
| /// Given a list of `times` and a target value, get the interpolation relationship for the
 | ||||
| /// target value in terms of the indices of the starting list. In a sense, this encapsulates the
 | ||||
| /// heart of uneven/keyframe sampling.
 | ||||
| ///
 | ||||
| /// `times` is assumed to be sorted, deduplicated, and consisting only of finite values. It is also
 | ||||
| /// assumed to contain at least two values.
 | ||||
| ///
 | ||||
| /// # Panics
 | ||||
| /// This function will panic if `times` contains NAN.
 | ||||
| pub fn uneven_interp(times: &[f32], t: f32) -> InterpolationDatum<usize> { | ||||
|     match times.binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) { | ||||
|         Ok(index) => InterpolationDatum::Exact(index), | ||||
|         Err(index) => { | ||||
|             if index == 0 { | ||||
|                 // This is before the first keyframe.
 | ||||
|                 InterpolationDatum::LeftTail(0) | ||||
|             } else if index >= times.len() { | ||||
|                 // This is after the last keyframe.
 | ||||
|                 InterpolationDatum::RightTail(times.len() - 1) | ||||
|             } else { | ||||
|                 // This is actually in the middle somewhere.
 | ||||
|                 let t_lower = times[index - 1]; | ||||
|                 let t_upper = times[index]; | ||||
|                 let s = (t - t_lower) / (t_upper - t_lower); | ||||
|                 InterpolationDatum::Between(index - 1, index, s) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -2,10 +2,14 @@ | ||||
| //! contains the [`Interval`] type, along with a selection of core data structures used to back
 | ||||
| //! curves that are interpolated from samples.
 | ||||
| 
 | ||||
| pub mod cores; | ||||
| pub mod interval; | ||||
| 
 | ||||
| pub use interval::{interval, Interval}; | ||||
| use itertools::Itertools; | ||||
| 
 | ||||
| use crate::StableInterpolate; | ||||
| use cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError}; | ||||
| use interval::InvalidIntervalError; | ||||
| use std::{marker::PhantomData, ops::Deref}; | ||||
| use thiserror::Error; | ||||
| @ -50,6 +54,7 @@ pub trait Curve<T> { | ||||
|     /// Create a new curve by mapping the values of this curve via a function `f`; i.e., if the
 | ||||
|     /// sample at time `t` for this curve is `x`, the value at time `t` on the new curve will be
 | ||||
|     /// `f(x)`.
 | ||||
|     #[must_use] | ||||
|     fn map<S, F>(self, f: F) -> MapCurve<T, S, Self, F> | ||||
|     where | ||||
|         Self: Sized, | ||||
| @ -98,6 +103,7 @@ pub trait Curve<T> { | ||||
|     /// let domain = my_curve.domain();
 | ||||
|     /// let eased_curve = my_curve.reparametrize(domain, |t| easing_curve.sample_unchecked(t).y);
 | ||||
|     /// ```
 | ||||
|     #[must_use] | ||||
|     fn reparametrize<F>(self, domain: Interval, f: F) -> ReparamCurve<T, Self, F> | ||||
|     where | ||||
|         Self: Sized, | ||||
| @ -142,6 +148,7 @@ pub trait Curve<T> { | ||||
|     /// The resulting curve samples at time `t` by first sampling `other` at time `t`, which produces
 | ||||
|     /// another sample time `s` which is then used to sample this curve. The domain of the resulting
 | ||||
|     /// curve is the domain of `other`.
 | ||||
|     #[must_use] | ||||
|     fn reparametrize_by_curve<C>(self, other: C) -> CurveReparamCurve<T, Self, C> | ||||
|     where | ||||
|         Self: Sized, | ||||
| @ -160,6 +167,7 @@ pub trait Curve<T> { | ||||
|     /// For example, if this curve outputs `x` at time `t`, then the produced curve will produce
 | ||||
|     /// `(t, x)` at time `t`. In particular, if this curve is a `Curve<T>`, the output of this method
 | ||||
|     /// is a `Curve<(f32, T)>`.
 | ||||
|     #[must_use] | ||||
|     fn graph(self) -> GraphCurve<T, Self> | ||||
|     where | ||||
|         Self: Sized, | ||||
| @ -212,12 +220,171 @@ pub trait Curve<T> { | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally
 | ||||
|     /// spaced sample values, using the provided `interpolation` to interpolate between adjacent samples.
 | ||||
|     /// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1,
 | ||||
|     /// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at
 | ||||
|     /// the midpoint is taken as well, and so on. If `segments` is zero, or if this curve has an unbounded
 | ||||
|     /// domain, then a [`ResamplingError`] is returned.
 | ||||
|     ///
 | ||||
|     /// The interpolation takes two values by reference together with a scalar parameter and
 | ||||
|     /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
 | ||||
|     /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
 | ||||
|     ///
 | ||||
|     /// # Example
 | ||||
|     /// ```
 | ||||
|     /// # use bevy_math::*;
 | ||||
|     /// # use bevy_math::curve::*;
 | ||||
|     /// let quarter_rotation = function_curve(interval(0.0, 90.0).unwrap(), |t| Rot2::degrees(t));
 | ||||
|     /// // A curve which only stores three data points and uses `nlerp` to interpolate them:
 | ||||
|     /// let resampled_rotation = quarter_rotation.resample(3, |x, y, t| x.nlerp(*y, t));
 | ||||
|     /// ```
 | ||||
|     fn resample<I>( | ||||
|         &self, | ||||
|         segments: usize, | ||||
|         interpolation: I, | ||||
|     ) -> Result<SampleCurve<T, I>, ResamplingError> | ||||
|     where | ||||
|         Self: Sized, | ||||
|         I: Fn(&T, &T, f32) -> T, | ||||
|     { | ||||
|         let samples = self.samples(segments + 1)?.collect_vec(); | ||||
|         Ok(SampleCurve { | ||||
|             core: EvenCore { | ||||
|                 domain: self.domain(), | ||||
|                 samples, | ||||
|             }, | ||||
|             interpolation, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally
 | ||||
|     /// spaced sample values, using [automatic interpolation] to interpolate between adjacent samples.
 | ||||
|     /// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1,
 | ||||
|     /// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at
 | ||||
|     /// the midpoint is taken as well, and so on. If `segments` is zero, or if this curve has an unbounded
 | ||||
|     /// domain, then a [`ResamplingError`] is returned.
 | ||||
|     ///
 | ||||
|     /// [automatic interpolation]: crate::common_traits::StableInterpolate
 | ||||
|     fn resample_auto(&self, segments: usize) -> Result<SampleAutoCurve<T>, ResamplingError> | ||||
|     where | ||||
|         Self: Sized, | ||||
|         T: StableInterpolate, | ||||
|     { | ||||
|         let samples = self.samples(segments + 1)?.collect_vec(); | ||||
|         Ok(SampleAutoCurve { | ||||
|             core: EvenCore { | ||||
|                 domain: self.domain(), | ||||
|                 samples, | ||||
|             }, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /// Extract an iterator over evenly-spaced samples from this curve. If `samples` is less than 2
 | ||||
|     /// or if this curve has unbounded domain, then an error is returned instead.
 | ||||
|     fn samples(&self, samples: usize) -> Result<impl Iterator<Item = T>, ResamplingError> | ||||
|     where | ||||
|         Self: Sized, | ||||
|     { | ||||
|         if samples < 2 { | ||||
|             return Err(ResamplingError::NotEnoughSamples(samples)); | ||||
|         } | ||||
|         if !self.domain().is_bounded() { | ||||
|             return Err(ResamplingError::UnboundedDomain); | ||||
|         } | ||||
| 
 | ||||
|         // Unwrap on `spaced_points` always succeeds because its error conditions are handled
 | ||||
|         // above.
 | ||||
|         Ok(self | ||||
|             .domain() | ||||
|             .spaced_points(samples) | ||||
|             .unwrap() | ||||
|             .map(|t| self.sample_unchecked(t))) | ||||
|     } | ||||
| 
 | ||||
|     /// Resample this [`Curve`] to produce a new one that is defined by interpolation over samples
 | ||||
|     /// taken at a given set of times. The given `interpolation` is used to interpolate adjacent
 | ||||
|     /// samples, and the `sample_times` are expected to contain at least two valid times within the
 | ||||
|     /// curve's domain interval.
 | ||||
|     ///
 | ||||
|     /// Redundant sample times, non-finite sample times, and sample times outside of the domain
 | ||||
|     /// are simply filtered out. With an insufficient quantity of data, a [`ResamplingError`] is
 | ||||
|     /// returned.
 | ||||
|     ///
 | ||||
|     /// The domain of the produced curve stretches between the first and last sample times of the
 | ||||
|     /// iterator.
 | ||||
|     ///
 | ||||
|     /// The interpolation takes two values by reference together with a scalar parameter and
 | ||||
|     /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
 | ||||
|     /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
 | ||||
|     fn resample_uneven<I>( | ||||
|         &self, | ||||
|         sample_times: impl IntoIterator<Item = f32>, | ||||
|         interpolation: I, | ||||
|     ) -> Result<UnevenSampleCurve<T, I>, ResamplingError> | ||||
|     where | ||||
|         Self: Sized, | ||||
|         I: Fn(&T, &T, f32) -> T, | ||||
|     { | ||||
|         let domain = self.domain(); | ||||
|         let mut times = sample_times | ||||
|             .into_iter() | ||||
|             .filter(|t| t.is_finite() && domain.contains(*t)) | ||||
|             .collect_vec(); | ||||
|         times.sort_by(f32::total_cmp); | ||||
|         times.dedup(); | ||||
|         if times.len() < 2 { | ||||
|             return Err(ResamplingError::NotEnoughSamples(times.len())); | ||||
|         } | ||||
|         let samples = times.iter().map(|t| self.sample_unchecked(*t)).collect(); | ||||
|         Ok(UnevenSampleCurve { | ||||
|             core: UnevenCore { times, samples }, | ||||
|             interpolation, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /// Resample this [`Curve`] to produce a new one that is defined by [automatic interpolation] over
 | ||||
|     /// samples taken at the given set of times. The given `sample_times` are expected to contain at least
 | ||||
|     /// two valid times within the curve's domain interval.
 | ||||
|     ///
 | ||||
|     /// Redundant sample times, non-finite sample times, and sample times outside of the domain
 | ||||
|     /// are simply filtered out. With an insufficient quantity of data, a [`ResamplingError`] is
 | ||||
|     /// returned.
 | ||||
|     ///
 | ||||
|     /// The domain of the produced [`UnevenSampleAutoCurve`] stretches between the first and last
 | ||||
|     /// sample times of the iterator.
 | ||||
|     ///
 | ||||
|     /// [automatic interpolation]: crate::common_traits::StableInterpolate
 | ||||
|     fn resample_uneven_auto( | ||||
|         &self, | ||||
|         sample_times: impl IntoIterator<Item = f32>, | ||||
|     ) -> Result<UnevenSampleAutoCurve<T>, ResamplingError> | ||||
|     where | ||||
|         Self: Sized, | ||||
|         T: StableInterpolate, | ||||
|     { | ||||
|         let domain = self.domain(); | ||||
|         let mut times = sample_times | ||||
|             .into_iter() | ||||
|             .filter(|t| t.is_finite() && domain.contains(*t)) | ||||
|             .collect_vec(); | ||||
|         times.sort_by(f32::total_cmp); | ||||
|         times.dedup(); | ||||
|         if times.len() < 2 { | ||||
|             return Err(ResamplingError::NotEnoughSamples(times.len())); | ||||
|         } | ||||
|         let samples = times.iter().map(|t| self.sample_unchecked(*t)).collect(); | ||||
|         Ok(UnevenSampleAutoCurve { | ||||
|             core: UnevenCore { times, samples }, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /// Borrow this curve rather than taking ownership of it. This is essentially an alias for a
 | ||||
|     /// prefix `&`; the point is that intermediate operations can be performed while retaining
 | ||||
|     /// access to the original curve.
 | ||||
|     ///
 | ||||
|     /// # Example
 | ||||
|     /// ```ignore
 | ||||
|     /// ```
 | ||||
|     /// # use bevy_math::curve::*;
 | ||||
|     /// let my_curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t * t + 1.0);
 | ||||
|     /// // Borrow `my_curve` long enough to resample a mapped version. Note that `map` takes
 | ||||
| @ -234,6 +401,7 @@ pub trait Curve<T> { | ||||
|     } | ||||
| 
 | ||||
|     /// Flip this curve so that its tuple output is arranged the other way.
 | ||||
|     #[must_use] | ||||
|     fn flip<U, V>(self) -> impl Curve<(V, U)> | ||||
|     where | ||||
|         Self: Sized + Curve<(U, V)>, | ||||
| @ -284,6 +452,20 @@ pub enum ChainError { | ||||
|     SecondStartInfinite, | ||||
| } | ||||
| 
 | ||||
| /// An error indicating that a resampling operation could not be performed because of
 | ||||
| /// malformed inputs.
 | ||||
| #[derive(Debug, Error)] | ||||
| #[error("Could not resample from this curve because of bad inputs")] | ||||
| pub enum ResamplingError { | ||||
|     /// This resampling operation was not provided with enough samples to have well-formed output.
 | ||||
|     #[error("Not enough unique samples to construct resampled curve")] | ||||
|     NotEnoughSamples(usize), | ||||
| 
 | ||||
|     /// This resampling operation failed because of an unbounded interval.
 | ||||
|     #[error("Could not resample because this curve has unbounded domain")] | ||||
|     UnboundedDomain, | ||||
| } | ||||
| 
 | ||||
| /// A curve with a constant value over its domain.
 | ||||
| ///
 | ||||
| /// This is a curve that holds an inner value and always produces a clone of that value when sampled.
 | ||||
| @ -575,6 +757,199 @@ where | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// A curve that is defined by explicit neighbor interpolation over a set of samples.
 | ||||
| #[derive(Clone, Debug)] | ||||
| #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] | ||||
| #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] | ||||
| pub struct SampleCurve<T, I> { | ||||
|     core: EvenCore<T>, | ||||
|     interpolation: I, | ||||
| } | ||||
| 
 | ||||
| impl<T, I> Curve<T> for SampleCurve<T, I> | ||||
| where | ||||
|     T: Clone, | ||||
|     I: Fn(&T, &T, f32) -> T, | ||||
| { | ||||
|     #[inline] | ||||
|     fn domain(&self) -> Interval { | ||||
|         self.core.domain() | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     fn sample_unchecked(&self, t: f32) -> T { | ||||
|         self.core.sample_with(t, &self.interpolation) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<T, I> SampleCurve<T, I> { | ||||
|     /// Create a new [`SampleCurve`] using the specified `interpolation` to interpolate between
 | ||||
|     /// the given `samples`. An error is returned if there are not at least 2 samples or if the
 | ||||
|     /// given `domain` is unbounded.
 | ||||
|     ///
 | ||||
|     /// The interpolation takes two values by reference together with a scalar parameter and
 | ||||
|     /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
 | ||||
|     /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
 | ||||
|     pub fn new( | ||||
|         domain: Interval, | ||||
|         samples: impl IntoIterator<Item = T>, | ||||
|         interpolation: I, | ||||
|     ) -> Result<Self, EvenCoreError> | ||||
|     where | ||||
|         I: Fn(&T, &T, f32) -> T, | ||||
|     { | ||||
|         Ok(Self { | ||||
|             core: EvenCore::new(domain, samples)?, | ||||
|             interpolation, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// A curve that is defined by neighbor interpolation over a set of samples.
 | ||||
| #[derive(Clone, Debug)] | ||||
| #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] | ||||
| #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] | ||||
| pub struct SampleAutoCurve<T> { | ||||
|     core: EvenCore<T>, | ||||
| } | ||||
| 
 | ||||
| impl<T> Curve<T> for SampleAutoCurve<T> | ||||
| where | ||||
|     T: StableInterpolate, | ||||
| { | ||||
|     #[inline] | ||||
|     fn domain(&self) -> Interval { | ||||
|         self.core.domain() | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     fn sample_unchecked(&self, t: f32) -> T { | ||||
|         self.core | ||||
|             .sample_with(t, <T as StableInterpolate>::interpolate_stable) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<T> SampleAutoCurve<T> { | ||||
|     /// Create a new [`SampleCurve`] using type-inferred interpolation to interpolate between
 | ||||
|     /// the given `samples`. An error is returned if there are not at least 2 samples or if the
 | ||||
|     /// given `domain` is unbounded.
 | ||||
|     pub fn new( | ||||
|         domain: Interval, | ||||
|         samples: impl IntoIterator<Item = T>, | ||||
|     ) -> Result<Self, EvenCoreError> { | ||||
|         Ok(Self { | ||||
|             core: EvenCore::new(domain, samples)?, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// A curve that is defined by interpolation over unevenly spaced samples with explicit
 | ||||
| /// interpolation.
 | ||||
| #[derive(Clone, Debug)] | ||||
| #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] | ||||
| #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] | ||||
| pub struct UnevenSampleCurve<T, I> { | ||||
|     core: UnevenCore<T>, | ||||
|     interpolation: I, | ||||
| } | ||||
| 
 | ||||
| impl<T, I> Curve<T> for UnevenSampleCurve<T, I> | ||||
| where | ||||
|     T: Clone, | ||||
|     I: Fn(&T, &T, f32) -> T, | ||||
| { | ||||
|     #[inline] | ||||
|     fn domain(&self) -> Interval { | ||||
|         self.core.domain() | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     fn sample_unchecked(&self, t: f32) -> T { | ||||
|         self.core.sample_with(t, &self.interpolation) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<T, I> UnevenSampleCurve<T, I> { | ||||
|     /// Create a new [`UnevenSampleCurve`] using the provided `interpolation` to interpolate
 | ||||
|     /// between adjacent `timed_samples`. The given samples are filtered to finite times and
 | ||||
|     /// sorted internally; if there are not at least 2 valid timed samples, an error will be
 | ||||
|     /// returned.
 | ||||
|     ///
 | ||||
|     /// The interpolation takes two values by reference together with a scalar parameter and
 | ||||
|     /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
 | ||||
|     /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
 | ||||
|     pub fn new( | ||||
|         timed_samples: impl IntoIterator<Item = (f32, T)>, | ||||
|         interpolation: I, | ||||
|     ) -> Result<Self, UnevenCoreError> { | ||||
|         Ok(Self { | ||||
|             core: UnevenCore::new(timed_samples)?, | ||||
|             interpolation, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /// This [`UnevenSampleAutoCurve`], but with the sample times moved by the map `f`.
 | ||||
|     /// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`],
 | ||||
|     /// but the function inputs to each are inverses of one another.
 | ||||
|     ///
 | ||||
|     /// The samples are re-sorted by time after mapping and deduplicated by output time, so
 | ||||
|     /// the function `f` should generally be injective over the sample times of the curve.
 | ||||
|     pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve<T, I> { | ||||
|         Self { | ||||
|             core: self.core.map_sample_times(f), | ||||
|             interpolation: self.interpolation, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// A curve that is defined by interpolation over unevenly spaced samples.
 | ||||
| #[derive(Clone, Debug)] | ||||
| #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] | ||||
| #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] | ||||
| pub struct UnevenSampleAutoCurve<T> { | ||||
|     core: UnevenCore<T>, | ||||
| } | ||||
| 
 | ||||
| impl<T> Curve<T> for UnevenSampleAutoCurve<T> | ||||
| where | ||||
|     T: StableInterpolate, | ||||
| { | ||||
|     #[inline] | ||||
|     fn domain(&self) -> Interval { | ||||
|         self.core.domain() | ||||
|     } | ||||
| 
 | ||||
|     #[inline] | ||||
|     fn sample_unchecked(&self, t: f32) -> T { | ||||
|         self.core | ||||
|             .sample_with(t, <T as StableInterpolate>::interpolate_stable) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<T> UnevenSampleAutoCurve<T> { | ||||
|     /// Create a new [`UnevenSampleAutoCurve`] from a given set of timed samples, interpolated
 | ||||
|     /// using the  The samples are filtered to finite times and
 | ||||
|     /// sorted internally; if there are not at least 2 valid timed samples, an error will be
 | ||||
|     /// returned.
 | ||||
|     pub fn new(timed_samples: impl IntoIterator<Item = (f32, T)>) -> Result<Self, UnevenCoreError> { | ||||
|         Ok(Self { | ||||
|             core: UnevenCore::new(timed_samples)?, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /// This [`UnevenSampleAutoCurve`], but with the sample times moved by the map `f`.
 | ||||
|     /// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`],
 | ||||
|     /// but the function inputs to each are inverses of one another.
 | ||||
|     ///
 | ||||
|     /// The samples are re-sorted by time after mapping and deduplicated by output time, so
 | ||||
|     /// the function `f` should generally be injective over the sample times of the curve.
 | ||||
|     pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleAutoCurve<T> { | ||||
|         Self { | ||||
|             core: self.core.map_sample_times(f), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Create a [`Curve`] that constantly takes the given `value` over the given `domain`.
 | ||||
| pub fn constant_curve<T: Clone>(domain: Interval, value: T) -> ConstantCurve<T> { | ||||
|     ConstantCurve { domain, value } | ||||
| @ -682,4 +1057,71 @@ mod tests { | ||||
|         assert_abs_diff_eq!(second_reparam.sample_unchecked(0.5), 1.5); | ||||
|         assert_abs_diff_eq!(second_reparam.sample_unchecked(1.0), 2.0); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn resampling() { | ||||
|         let curve = function_curve(interval(1.0, 4.0).unwrap(), ops::log2); | ||||
| 
 | ||||
|         // Need at least one segment to sample.
 | ||||
|         let nice_try = curve.by_ref().resample_auto(0); | ||||
|         assert!(nice_try.is_err()); | ||||
| 
 | ||||
|         // The values of a resampled curve should be very close at the sample points.
 | ||||
|         // Because of denominators, it's not literally equal.
 | ||||
|         // (This is a tradeoff against O(1) sampling.)
 | ||||
|         let resampled_curve = curve.by_ref().resample_auto(100).unwrap(); | ||||
|         for test_pt in curve.domain().spaced_points(101).unwrap() { | ||||
|             let expected = curve.sample_unchecked(test_pt); | ||||
|             assert_abs_diff_eq!( | ||||
|                 resampled_curve.sample_unchecked(test_pt), | ||||
|                 expected, | ||||
|                 epsilon = 1e-6 | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         // Another example.
 | ||||
|         let curve = function_curve(interval(0.0, TAU).unwrap(), ops::cos); | ||||
|         let resampled_curve = curve.by_ref().resample_auto(1000).unwrap(); | ||||
|         for test_pt in curve.domain().spaced_points(1001).unwrap() { | ||||
|             let expected = curve.sample_unchecked(test_pt); | ||||
|             assert_abs_diff_eq!( | ||||
|                 resampled_curve.sample_unchecked(test_pt), | ||||
|                 expected, | ||||
|                 epsilon = 1e-6 | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn uneven_resampling() { | ||||
|         let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), ops::exp); | ||||
| 
 | ||||
|         // Need at least two points to resample.
 | ||||
|         let nice_try = curve.by_ref().resample_uneven_auto([1.0; 1]); | ||||
|         assert!(nice_try.is_err()); | ||||
| 
 | ||||
|         // Uneven sampling should produce literal equality at the sample points.
 | ||||
|         // (This is part of what you get in exchange for O(log(n)) sampling.)
 | ||||
|         let sample_points = (0..100).map(|idx| idx as f32 * 0.1); | ||||
|         let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap(); | ||||
|         for idx in 0..100 { | ||||
|             let test_pt = idx as f32 * 0.1; | ||||
|             let expected = curve.sample_unchecked(test_pt); | ||||
|             assert_eq!(resampled_curve.sample_unchecked(test_pt), expected); | ||||
|         } | ||||
|         assert_abs_diff_eq!(resampled_curve.domain().start(), 0.0); | ||||
|         assert_abs_diff_eq!(resampled_curve.domain().end(), 9.9, epsilon = 1e-6); | ||||
| 
 | ||||
|         // Another example.
 | ||||
|         let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), ops::log2); | ||||
|         let sample_points = (0..10).map(|idx| ops::exp2(idx as f32)); | ||||
|         let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap(); | ||||
|         for idx in 0..10 { | ||||
|             let test_pt = ops::exp2(idx as f32); | ||||
|             let expected = curve.sample_unchecked(test_pt); | ||||
|             assert_eq!(resampled_curve.sample_unchecked(test_pt), expected); | ||||
|         } | ||||
|         assert_abs_diff_eq!(resampled_curve.domain().start(), 1.0); | ||||
|         assert_abs_diff_eq!(resampled_curve.domain().end(), 512.0); | ||||
|     } | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Matty
						Matty