mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
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
2 changed files with 1071 additions and 1 deletions
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 a new issue