//! Core data structures to be used internally in Curve implementations, encapsulating storage1//! and access patterns for reuse.2//!3//! The `Core` types here expose their fields publicly so that it is easier to manipulate and4//! extend them, but in doing so, you must maintain the invariants of those fields yourself. The5//! provided methods all maintain the invariants, so this is only a concern if you manually mutate6//! the fields.78use crate::ops;910use super::interval::Interval;11use core::fmt::Debug;12use thiserror::Error;1314#[cfg(feature = "alloc")]15use {alloc::vec::Vec, itertools::Itertools};1617#[cfg(feature = "bevy_reflect")]18use bevy_reflect::Reflect;1920/// This type expresses the relationship of a value to a fixed collection of values. It is a kind21/// of summary used intermediately by sampling operations.22#[derive(Debug, Copy, Clone, PartialEq)]23#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]24#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]25pub enum InterpolationDatum<T> {26/// This value lies exactly on a value in the family.27Exact(T),2829/// This value is off the left tail of the family; the inner value is the family's leftmost.30LeftTail(T),3132/// This value is off the right tail of the family; the inner value is the family's rightmost.33RightTail(T),3435/// This value lies on the interior, in between two points, with a third parameter expressing36/// the interpolation factor between the two.37Between(T, T, f32),38}3940impl<T> InterpolationDatum<T> {41/// Map all values using a given function `f`, leaving the interpolation parameters in any42/// [`Between`] variants unchanged.43///44/// [`Between`]: `InterpolationDatum::Between`45#[must_use]46pub fn map<S>(self, f: impl Fn(T) -> S) -> InterpolationDatum<S> {47match self {48InterpolationDatum::Exact(v) => InterpolationDatum::Exact(f(v)),49InterpolationDatum::LeftTail(v) => InterpolationDatum::LeftTail(f(v)),50InterpolationDatum::RightTail(v) => InterpolationDatum::RightTail(f(v)),51InterpolationDatum::Between(u, v, s) => InterpolationDatum::Between(f(u), f(v), s),52}53}54}5556/// The data core of a curve derived from evenly-spaced samples. The intention is to use this57/// in addition to explicit or inferred interpolation information in user-space in order to58/// implement curves using [`domain`] and [`sample_with`].59///60/// The internals are made transparent to give curve authors freedom, but [the provided constructor]61/// enforces the required invariants, and the methods maintain those invariants.62///63/// [the provided constructor]: EvenCore::new64/// [`domain`]: EvenCore::domain65/// [`sample_with`]: EvenCore::sample_with66///67/// # Example68/// ```rust69/// # use bevy_math::curve::*;70/// # use bevy_math::curve::cores::*;71/// // Let's make a curve that interpolates evenly spaced samples using either linear interpolation72/// // or step "interpolation" — i.e. just using the most recent sample as the source of truth.73/// enum InterpolationMode {74/// Linear,75/// Step,76/// }77///78/// // Linear interpolation mode is driven by a trait.79/// trait LinearInterpolate {80/// fn lerp(&self, other: &Self, t: f32) -> Self;81/// }82///83/// // Step interpolation just uses an explicit function.84/// fn step<T: Clone>(first: &T, second: &T, t: f32) -> T {85/// if t >= 1.0 {86/// second.clone()87/// } else {88/// first.clone()89/// }90/// }91///92/// // Omitted: Implementing `LinearInterpolate` on relevant types; e.g. `f32`, `Vec3`, and so on.93///94/// // The curve itself uses `EvenCore` to hold the evenly-spaced samples, and the `sample_with`95/// // function will do all the work of interpolating once given a function to do it with.96/// struct MyCurve<T> {97/// core: EvenCore<T>,98/// interpolation_mode: InterpolationMode,99/// }100///101/// impl<T> Curve<T> for MyCurve<T>102/// where103/// T: LinearInterpolate + Clone,104/// {105/// fn domain(&self) -> Interval {106/// self.core.domain()107/// }108///109/// fn sample_unchecked(&self, t: f32) -> T {110/// // To sample this curve, check the interpolation mode and dispatch accordingly.111/// match self.interpolation_mode {112/// InterpolationMode::Linear => self.core.sample_with(t, <T as LinearInterpolate>::lerp),113/// InterpolationMode::Step => self.core.sample_with(t, step),114/// }115/// }116/// }117/// ```118#[cfg(feature = "alloc")]119#[derive(Debug, Clone, PartialEq)]120#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]121#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]122pub struct EvenCore<T> {123/// The domain over which the samples are taken, which corresponds to the domain of the curve124/// formed by interpolating them.125///126/// # Invariants127/// This must always be a bounded interval; i.e. its endpoints must be finite.128pub domain: Interval,129130/// The samples that are interpolated to extract values.131///132/// # Invariants133/// This must always have a length of at least 2.134pub samples: Vec<T>,135}136137/// An error indicating that an [`EvenCore`] could not be constructed.138#[derive(Debug, Error)]139#[error("Could not construct an EvenCore")]140pub enum EvenCoreError {141/// Not enough samples were provided.142#[error("Need at least two samples to create an EvenCore, but {samples} were provided")]143NotEnoughSamples {144/// The number of samples that were provided.145samples: usize,146},147148/// Unbounded domains are not compatible with `EvenCore`.149#[error("Cannot create an EvenCore over an unbounded domain")]150UnboundedDomain,151}152153#[cfg(feature = "alloc")]154impl<T> EvenCore<T> {155/// Create a new [`EvenCore`] from the specified `domain` and `samples`. The samples are156/// regarded to be evenly spaced within the given domain interval, so that the outermost157/// samples form the boundary of that interval. An error is returned if there are not at158/// least 2 samples or if the given domain is unbounded.159#[inline]160pub fn new(161domain: Interval,162samples: impl IntoIterator<Item = T>,163) -> Result<Self, EvenCoreError> {164let samples: Vec<T> = samples.into_iter().collect();165if samples.len() < 2 {166return Err(EvenCoreError::NotEnoughSamples {167samples: samples.len(),168});169}170if !domain.is_bounded() {171return Err(EvenCoreError::UnboundedDomain);172}173174Ok(EvenCore { domain, samples })175}176177/// The domain of the curve derived from this core.178#[inline]179pub const fn domain(&self) -> Interval {180self.domain181}182183/// Obtain a value from the held samples using the given `interpolation` to interpolate184/// between adjacent samples.185///186/// The interpolation takes two values by reference together with a scalar parameter and187/// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and188/// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.189#[inline]190pub fn sample_with<I>(&self, t: f32, interpolation: I) -> T191where192T: Clone,193I: Fn(&T, &T, f32) -> T,194{195match even_interp(self.domain, self.samples.len(), t) {196InterpolationDatum::Exact(idx)197| InterpolationDatum::LeftTail(idx)198| InterpolationDatum::RightTail(idx) => self.samples[idx].clone(),199InterpolationDatum::Between(lower_idx, upper_idx, s) => {200interpolation(&self.samples[lower_idx], &self.samples[upper_idx], s)201}202}203}204205/// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover206/// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can207/// be used to interpolate between the two contained values with the given parameter. The other208/// variants give additional context about where the value is relative to the family of samples.209///210/// [`Between`]: `InterpolationDatum::Between`211pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&T> {212even_interp(self.domain, self.samples.len(), t).map(|idx| &self.samples[idx])213}214215/// Like [`sample_interp`], but the returned values include the sample times. This can be216/// useful when sample interpolation is not scale-invariant.217///218/// [`sample_interp`]: EvenCore::sample_interp219pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> {220let segment_len = self.domain.length() / (self.samples.len() - 1) as f32;221even_interp(self.domain, self.samples.len(), t).map(|idx| {222(223self.domain.start() + segment_len * idx as f32,224&self.samples[idx],225)226})227}228}229230/// Given a domain and a number of samples taken over that interval, return an [`InterpolationDatum`]231/// that governs how samples are extracted relative to the stored data.232///233/// `domain` must be a bounded interval (i.e. `domain.is_bounded() == true`).234///235/// `samples` must be at least 2.236///237/// This function will never panic, but it may return invalid indices if its assumptions are violated.238pub fn even_interp(domain: Interval, samples: usize, t: f32) -> InterpolationDatum<usize> {239let subdivs = samples - 1;240let step = domain.length() / subdivs as f32;241let t_shifted = t - domain.start();242let steps_taken = t_shifted / step;243244if steps_taken <= 0.0 {245// To the left side of all the samples.246InterpolationDatum::LeftTail(0)247} else if steps_taken >= subdivs as f32 {248// To the right side of all the samples249InterpolationDatum::RightTail(samples - 1)250} else {251let lower_index = ops::floor(steps_taken) as usize;252// This upper index is always valid because `steps_taken` is a finite value253// strictly less than `samples - 1`, so its floor is at most `samples - 2`254let upper_index = lower_index + 1;255let s = ops::fract(steps_taken);256InterpolationDatum::Between(lower_index, upper_index, s)257}258}259260/// The data core of a curve defined by unevenly-spaced samples or keyframes. The intention is to261/// use this in concert with implicitly or explicitly-defined interpolation in user-space in262/// order to implement the curve interface using [`domain`] and [`sample_with`].263///264/// The internals are made transparent to give curve authors freedom, but [the provided constructor]265/// enforces the required invariants, and the methods maintain those invariants.266///267/// # Example268/// ```rust269/// # use bevy_math::curve::*;270/// # use bevy_math::curve::cores::*;271/// // Let's make a curve formed by interpolating rotations.272/// // We'll support two common modes of interpolation:273/// // - Normalized linear: First do linear interpolation, then normalize to get a valid rotation.274/// // - Spherical linear: Interpolate through valid rotations with constant angular velocity.275/// enum InterpolationMode {276/// NormalizedLinear,277/// SphericalLinear,278/// }279///280/// // Our interpolation modes will be driven by traits.281/// trait NormalizedLinearInterpolate {282/// fn nlerp(&self, other: &Self, t: f32) -> Self;283/// }284///285/// trait SphericalLinearInterpolate {286/// fn slerp(&self, other: &Self, t: f32) -> Self;287/// }288///289/// // Omitted: These traits would be implemented for `Rot2`, `Quat`, and other rotation representations.290///291/// // The curve itself just needs to use the curve core for keyframes, `UnevenCore`, which handles292/// // everything except for the explicit interpolation used.293/// struct RotationCurve<T> {294/// core: UnevenCore<T>,295/// interpolation_mode: InterpolationMode,296/// }297///298/// impl<T> Curve<T> for RotationCurve<T>299/// where300/// T: NormalizedLinearInterpolate + SphericalLinearInterpolate + Clone,301/// {302/// fn domain(&self) -> Interval {303/// self.core.domain()304/// }305///306/// fn sample_unchecked(&self, t: f32) -> T {307/// // To sample the curve, we just look at the interpolation mode and308/// // dispatch accordingly.309/// match self.interpolation_mode {310/// InterpolationMode::NormalizedLinear =>311/// self.core.sample_with(t, <T as NormalizedLinearInterpolate>::nlerp),312/// InterpolationMode::SphericalLinear =>313/// self.core.sample_with(t, <T as SphericalLinearInterpolate>::slerp),314/// }315/// }316/// }317/// ```318///319/// [`domain`]: UnevenCore::domain320/// [`sample_with`]: UnevenCore::sample_with321/// [the provided constructor]: UnevenCore::new322#[cfg(feature = "alloc")]323#[derive(Debug, Clone)]324#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]325#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]326pub struct UnevenCore<T> {327/// The times for the samples of this curve.328///329/// # Invariants330/// This must always have a length of at least 2, be sorted, and have no331/// duplicated or non-finite times.332pub times: Vec<f32>,333334/// The samples corresponding to the times for this curve.335///336/// # Invariants337/// This must always have the same length as `times`.338pub samples: Vec<T>,339}340341/// An error indicating that an [`UnevenCore`] could not be constructed.342#[derive(Debug, Error)]343#[error("Could not construct an UnevenCore")]344pub enum UnevenCoreError {345/// Not enough samples were provided.346#[error(347"Need at least two unique samples to create an UnevenCore, but {samples} were provided"348)]349NotEnoughSamples {350/// The number of samples that were provided.351samples: usize,352},353}354355#[cfg(feature = "alloc")]356impl<T> UnevenCore<T> {357/// Create a new [`UnevenCore`]. The given samples are filtered to finite times and358/// sorted internally; if there are not at least 2 valid timed samples, an error will be359/// returned.360pub fn new(timed_samples: impl IntoIterator<Item = (f32, T)>) -> Result<Self, UnevenCoreError> {361// Filter out non-finite sample times first so they don't interfere with sorting/deduplication.362let mut timed_samples = timed_samples363.into_iter()364.filter(|(t, _)| t.is_finite())365.collect_vec();366timed_samples367// Using `total_cmp` is fine because no NANs remain and because deduplication uses368// `PartialEq` anyway (so -0.0 and 0.0 will be considered equal later regardless).369.sort_by(|(t0, _), (t1, _)| t0.total_cmp(t1));370timed_samples.dedup_by_key(|(t, _)| *t);371372if timed_samples.len() < 2 {373return Err(UnevenCoreError::NotEnoughSamples {374samples: timed_samples.len(),375});376}377378let (times, samples): (Vec<f32>, Vec<T>) = timed_samples.into_iter().unzip();379Ok(UnevenCore { times, samples })380}381382/// The domain of the curve derived from this core.383///384/// # Panics385/// This method may panic if the type's invariants aren't satisfied.386#[inline]387pub fn domain(&self) -> Interval {388let start = self.times.first().unwrap();389let end = self.times.last().unwrap();390Interval::new(*start, *end).unwrap()391}392393/// Obtain a value from the held samples using the given `interpolation` to interpolate394/// between adjacent samples.395///396/// The interpolation takes two values by reference together with a scalar parameter and397/// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and398/// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.399#[inline]400pub fn sample_with<I>(&self, t: f32, interpolation: I) -> T401where402T: Clone,403I: Fn(&T, &T, f32) -> T,404{405match uneven_interp(&self.times, t) {406InterpolationDatum::Exact(idx)407| InterpolationDatum::LeftTail(idx)408| InterpolationDatum::RightTail(idx) => self.samples[idx].clone(),409InterpolationDatum::Between(lower_idx, upper_idx, s) => {410interpolation(&self.samples[lower_idx], &self.samples[upper_idx], s)411}412}413}414415/// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover416/// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can417/// be used to interpolate between the two contained values with the given parameter. The other418/// variants give additional context about where the value is relative to the family of samples.419///420/// [`Between`]: `InterpolationDatum::Between`421pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&T> {422uneven_interp(&self.times, t).map(|idx| &self.samples[idx])423}424425/// Like [`sample_interp`], but the returned values include the sample times. This can be426/// useful when sample interpolation is not scale-invariant.427///428/// [`sample_interp`]: UnevenCore::sample_interp429pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> {430uneven_interp(&self.times, t).map(|idx| (self.times[idx], &self.samples[idx]))431}432433/// This core, but with the sample times moved by the map `f`.434/// In principle, when `f` is monotone, this is equivalent to [`CurveExt::reparametrize`],435/// but the function inputs to each are inverses of one another.436///437/// The samples are re-sorted by time after mapping and deduplicated by output time, so438/// the function `f` should generally be injective over the set of sample times, otherwise439/// data will be deleted.440///441/// [`CurveExt::reparametrize`]: crate::curve::CurveExt::reparametrize442#[must_use]443pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenCore<T> {444let mut timed_samples = self445.times446.into_iter()447.map(f)448.zip(self.samples)449.collect_vec();450timed_samples.sort_by(|(t1, _), (t2, _)| t1.total_cmp(t2));451timed_samples.dedup_by_key(|(t, _)| *t);452(self.times, self.samples) = timed_samples.into_iter().unzip();453self454}455}456457/// The data core of a curve using uneven samples (i.e. keyframes), where each sample time458/// yields some fixed number of values — the [sampling width]. This may serve as storage for459/// curves that yield vectors or iterators, and in some cases, it may be useful for cache locality460/// if the sample type can effectively be encoded as a fixed-length slice of values.461///462/// [sampling width]: ChunkedUnevenCore::width463#[cfg(feature = "alloc")]464#[derive(Debug, Clone)]465#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]466#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]467pub struct ChunkedUnevenCore<T> {468/// The times, one for each sample.469///470/// # Invariants471/// This must always have a length of at least 2, be sorted, and have no duplicated or472/// non-finite times.473pub times: Vec<f32>,474475/// The values that are used in sampling. Each width-worth of these correspond to a single sample.476///477/// # Invariants478/// The length of this vector must always be some fixed integer multiple of that of `times`.479pub values: Vec<T>,480}481482/// An error that indicates that a [`ChunkedUnevenCore`] could not be formed.483#[derive(Debug, Error)]484#[error("Could not create a ChunkedUnevenCore")]485pub enum ChunkedUnevenCoreError {486/// The width of a `ChunkedUnevenCore` cannot be zero.487#[error("Chunk width must be at least 1")]488ZeroWidth,489490/// At least two sample times are necessary to interpolate in `ChunkedUnevenCore`.491#[error(492"Need at least two unique samples to create a ChunkedUnevenCore, but {samples} were provided"493)]494NotEnoughSamples {495/// The number of samples that were provided.496samples: usize,497},498499/// The length of the value buffer is supposed to be the `width` times the number of samples.500#[error("Expected {expected} total values based on width, but {actual} were provided")]501MismatchedLengths {502/// The expected length of the value buffer.503expected: usize,504/// The actual length of the value buffer.505actual: usize,506},507508/// Tried to infer the width, but the ratio of lengths wasn't an integer, so no such length exists.509#[error("The length of the list of values ({values_len}) was not divisible by that of the list of times ({times_len})")]510NonDivisibleLengths {511/// The length of the value buffer.512values_len: usize,513/// The length of the time buffer.514times_len: usize,515},516}517518#[cfg(feature = "alloc")]519impl<T> ChunkedUnevenCore<T> {520/// Create a new [`ChunkedUnevenCore`]. The given `times` are sorted, filtered to finite times,521/// and deduplicated. See the [type-level documentation] for more information about this type.522///523/// Produces an error in any of the following circumstances:524/// - `width` is zero.525/// - `times` has less than `2` unique valid entries.526/// - `values` has the incorrect length relative to `times`.527///528/// [type-level documentation]: ChunkedUnevenCore529pub fn new(530times: impl IntoIterator<Item = f32>,531values: impl IntoIterator<Item = T>,532width: usize,533) -> Result<Self, ChunkedUnevenCoreError> {534let times = times.into_iter().collect_vec();535let values = values.into_iter().collect_vec();536537if width == 0 {538return Err(ChunkedUnevenCoreError::ZeroWidth);539}540541let times = filter_sort_dedup_times(times);542543if times.len() < 2 {544return Err(ChunkedUnevenCoreError::NotEnoughSamples {545samples: times.len(),546});547}548549if values.len() != times.len() * width {550return Err(ChunkedUnevenCoreError::MismatchedLengths {551expected: times.len() * width,552actual: values.len(),553});554}555556Ok(Self { times, values })557}558559/// Create a new [`ChunkedUnevenCore`], inferring the width from the sizes of the inputs.560/// The given `times` are sorted, filtered to finite times, and deduplicated. See the561/// [type-level documentation] for more information about this type. Prefer using [`new`]562/// if possible, since that constructor has richer error checking.563///564/// Produces an error in any of the following circumstances:565/// - `values` has length zero.566/// - `times` has less than `2` unique valid entries.567/// - The length of `values` is not divisible by that of `times` (once sorted, filtered,568/// and deduplicated).569///570/// The [width] is implicitly taken to be the length of `values` divided by that of `times`571/// (once sorted, filtered, and deduplicated).572///573/// [type-level documentation]: ChunkedUnevenCore574/// [`new`]: ChunkedUnevenCore::new575/// [width]: ChunkedUnevenCore::width576pub fn new_width_inferred(577times: impl IntoIterator<Item = f32>,578values: impl IntoIterator<Item = T>,579) -> Result<Self, ChunkedUnevenCoreError> {580let times = times.into_iter().collect_vec();581let values = values.into_iter().collect_vec();582583let times = filter_sort_dedup_times(times);584585if times.len() < 2 {586return Err(ChunkedUnevenCoreError::NotEnoughSamples {587samples: times.len(),588});589}590591if values.len() % times.len() != 0 {592return Err(ChunkedUnevenCoreError::NonDivisibleLengths {593values_len: values.len(),594times_len: times.len(),595});596}597598if values.is_empty() {599return Err(ChunkedUnevenCoreError::ZeroWidth);600}601602Ok(Self { times, values })603}604605/// The domain of the curve derived from this core.606///607/// # Panics608/// This may panic if this type's invariants aren't met.609#[inline]610pub fn domain(&self) -> Interval {611let start = self.times.first().unwrap();612let end = self.times.last().unwrap();613Interval::new(*start, *end).unwrap()614}615616/// The sample width: the number of values that are contained in each sample.617#[inline]618pub fn width(&self) -> usize {619self.values.len() / self.times.len()620}621622/// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover623/// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can624/// be used to interpolate between the two contained values with the given parameter. The other625/// variants give additional context about where the value is relative to the family of samples.626///627/// [`Between`]: `InterpolationDatum::Between`628#[inline]629pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&[T]> {630uneven_interp(&self.times, t).map(|idx| self.time_index_to_slice(idx))631}632633/// Like [`sample_interp`], but the returned values include the sample times. This can be634/// useful when sample interpolation is not scale-invariant.635///636/// [`sample_interp`]: ChunkedUnevenCore::sample_interp637pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &[T])> {638uneven_interp(&self.times, t).map(|idx| (self.times[idx], self.time_index_to_slice(idx)))639}640641/// Given an index in [times], returns the slice of [values] that correspond to the sample at642/// that time.643///644/// [times]: ChunkedUnevenCore::times645/// [values]: ChunkedUnevenCore::values646#[inline]647fn time_index_to_slice(&self, idx: usize) -> &[T] {648let width = self.width();649let lower_idx = width * idx;650let upper_idx = lower_idx + width;651&self.values[lower_idx..upper_idx]652}653}654655/// Sort the given times, deduplicate them, and filter them to only finite times.656#[cfg(feature = "alloc")]657fn filter_sort_dedup_times(times: impl IntoIterator<Item = f32>) -> Vec<f32> {658// Filter before sorting/deduplication so that NAN doesn't interfere with them.659let mut times = times.into_iter().filter(|t| t.is_finite()).collect_vec();660times.sort_by(f32::total_cmp);661times.dedup();662times663}664665/// Given a list of `times` and a target value, get the interpolation relationship for the666/// target value in terms of the indices of the starting list. In a sense, this encapsulates the667/// heart of uneven/keyframe sampling.668///669/// `times` is assumed to be sorted, deduplicated, and consisting only of finite values. It is also670/// assumed to contain at least two values.671///672/// # Panics673/// This function will panic if `times` contains NAN.674pub fn uneven_interp(times: &[f32], t: f32) -> InterpolationDatum<usize> {675match times.binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) {676Ok(index) => InterpolationDatum::Exact(index),677Err(index) => {678if index == 0 {679// This is before the first keyframe.680InterpolationDatum::LeftTail(0)681} else if index >= times.len() {682// This is after the last keyframe.683InterpolationDatum::RightTail(times.len() - 1)684} else {685// This is actually in the middle somewhere.686let t_lower = times[index - 1];687let t_upper = times[index];688let s = (t - t_lower) / (t_upper - t_lower);689InterpolationDatum::Between(index - 1, index, s)690}691}692}693}694695#[cfg(all(test, feature = "alloc"))]696mod tests {697use super::{ChunkedUnevenCore, EvenCore, UnevenCore};698use crate::curve::{cores::InterpolationDatum, interval};699use alloc::vec;700use approx::{assert_abs_diff_eq, AbsDiffEq};701702fn approx_between<T>(datum: InterpolationDatum<T>, start: T, end: T, p: f32) -> bool703where704T: PartialEq,705{706if let InterpolationDatum::Between(m_start, m_end, m_p) = datum {707m_start == start && m_end == end && m_p.abs_diff_eq(&p, 1e-6)708} else {709false710}711}712713fn is_left_tail<T>(datum: InterpolationDatum<T>) -> bool {714matches!(datum, InterpolationDatum::LeftTail(_))715}716717fn is_right_tail<T>(datum: InterpolationDatum<T>) -> bool {718matches!(datum, InterpolationDatum::RightTail(_))719}720721fn is_exact<T>(datum: InterpolationDatum<T>, target: T) -> bool722where723T: PartialEq,724{725if let InterpolationDatum::Exact(v) = datum {726v == target727} else {728false729}730}731732#[test]733fn even_sample_interp() {734let even_core = EvenCore::<f32>::new(735interval(0.0, 1.0).unwrap(),736// 11 entries -> 10 segments737vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0],738)739.expect("Failed to construct test core");740741let datum = even_core.sample_interp(-1.0);742assert!(is_left_tail(datum));743let datum = even_core.sample_interp(0.0);744assert!(is_left_tail(datum));745let datum = even_core.sample_interp(1.0);746assert!(is_right_tail(datum));747let datum = even_core.sample_interp(2.0);748assert!(is_right_tail(datum));749750let datum = even_core.sample_interp(0.05);751let InterpolationDatum::Between(0.0, 1.0, p) = datum else {752panic!("Sample did not lie in the correct subinterval")753};754assert_abs_diff_eq!(p, 0.5);755756let datum = even_core.sample_interp(0.05);757assert!(approx_between(datum, &0.0, &1.0, 0.5));758let datum = even_core.sample_interp(0.33);759assert!(approx_between(datum, &3.0, &4.0, 0.3));760let datum = even_core.sample_interp(0.78);761assert!(approx_between(datum, &7.0, &8.0, 0.8));762763let datum = even_core.sample_interp(0.5);764assert!(approx_between(datum, &4.0, &5.0, 1.0) || approx_between(datum, &5.0, &6.0, 0.0));765let datum = even_core.sample_interp(0.7);766assert!(approx_between(datum, &6.0, &7.0, 1.0) || approx_between(datum, &7.0, &8.0, 0.0));767}768769#[test]770fn uneven_sample_interp() {771let uneven_core = UnevenCore::<f32>::new(vec![772(0.0, 0.0),773(1.0, 3.0),774(2.0, 9.0),775(4.0, 10.0),776(8.0, -5.0),777])778.expect("Failed to construct test core");779780let datum = uneven_core.sample_interp(-1.0);781assert!(is_left_tail(datum));782let datum = uneven_core.sample_interp(0.0);783assert!(is_exact(datum, &0.0));784let datum = uneven_core.sample_interp(8.0);785assert!(is_exact(datum, &(-5.0)));786let datum = uneven_core.sample_interp(9.0);787assert!(is_right_tail(datum));788789let datum = uneven_core.sample_interp(0.5);790assert!(approx_between(datum, &0.0, &3.0, 0.5));791let datum = uneven_core.sample_interp(2.5);792assert!(approx_between(datum, &9.0, &10.0, 0.25));793let datum = uneven_core.sample_interp(7.0);794assert!(approx_between(datum, &10.0, &(-5.0), 0.75));795796let datum = uneven_core.sample_interp(2.0);797assert!(is_exact(datum, &9.0));798let datum = uneven_core.sample_interp(4.0);799assert!(is_exact(datum, &10.0));800}801802#[test]803fn chunked_uneven_sample_interp() {804let core =805ChunkedUnevenCore::new(vec![0.0, 2.0, 8.0], vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0], 2)806.expect("Failed to construct test core");807808let datum = core.sample_interp(-1.0);809assert!(is_left_tail(datum));810let datum = core.sample_interp(0.0);811assert!(is_exact(datum, &[0.0, 1.0]));812let datum = core.sample_interp(8.0);813assert!(is_exact(datum, &[4.0, 5.0]));814let datum = core.sample_interp(10.0);815assert!(is_right_tail(datum));816817let datum = core.sample_interp(1.0);818assert!(approx_between(datum, &[0.0, 1.0], &[2.0, 3.0], 0.5));819let datum = core.sample_interp(3.0);820assert!(approx_between(datum, &[2.0, 3.0], &[4.0, 5.0], 1.0 / 6.0));821822let datum = core.sample_interp(2.0);823assert!(is_exact(datum, &[2.0, 3.0]));824}825}826827828