From ebe084d2c72852e6b980b2353eb51ced9604a292 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 5 Aug 2024 08:50:41 -0400 Subject: [PATCH 01/22] RFC implementation yoinked from curve-trait branch --- crates/bevy_math/src/curve/cores.rs | 554 +++++++++++++ crates/bevy_math/src/curve/interval.rs | 330 ++++++++ crates/bevy_math/src/curve/mod.rs | 1054 ++++++++++++++++++++++++ crates/bevy_math/src/lib.rs | 1 + 4 files changed, 1939 insertions(+) create mode 100644 crates/bevy_math/src/curve/cores.rs create mode 100644 crates/bevy_math/src/curve/interval.rs create mode 100644 crates/bevy_math/src/curve/mod.rs diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs new file mode 100644 index 0000000000000..30574362489b9 --- /dev/null +++ b/crates/bevy_math/src/curve/cores.rs @@ -0,0 +1,554 @@ +//! Core data structures to be used internally in Curve implementations, encapsulating storage +//! and access patterns for reuse. +//! +//! The `Core` types here expose their fields publically so that it is easier to extend them, +//! but in doing so, you must maintain the invariants of those fields yourself. The provided +//! methods all maintain the invariants, so this only concerns you if you manually mutate them. + +use super::interval::Interval; +use core::fmt::Debug; +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 { + /// 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 InterpolationDatum { + /// 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(self, f: impl Fn(T) -> S) -> InterpolationDatum { + 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. +/// +/// [the provided constructor]: EvenCore::new +/// [`domain`]: EvenCore::domain +/// [`sample_with`]: EvenCore::sample_with +/// +/// # Example +/// ```rust +/// # use bevy_math::curve::*; +/// # use bevy_math::curve::cores::*; +/// enum InterpolationMode { +/// Linear, +/// Step, +/// } +/// +/// trait LinearInterpolate { +/// fn lerp(&self, other: &Self, t: f32) -> Self; +/// } +/// +/// fn step(first: &T, second: &T, t: f32) -> T { +/// if t >= 1.0 { +/// second.clone() +/// } else { +/// first.clone() +/// } +/// } +/// +/// struct MyCurve { +/// core: EvenCore, +/// interpolation_mode: InterpolationMode, +/// } +/// +/// impl Curve for MyCurve +/// where +/// T: LinearInterpolate + Clone, +/// { +/// fn domain(&self) -> Interval { +/// self.core.domain() +/// } +/// +/// fn sample(&self, t: f32) -> T { +/// match self.interpolation_mode { +/// InterpolationMode::Linear => self.core.sample_with(t, ::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 { + /// 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, +} + +/// An error indicating that a [`EvenCore`] could not be constructed. +#[derive(Debug, Error, PartialEq, Eq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub enum EvenCoreError { + /// Not enough samples were provided. + #[error("Need at least two samples to create a 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 a domain with an infinite endpoint")] + InfiniteDomain, +} + +impl EvenCore { + /// Create a new [`EvenCore`] from the specified `domain` and `samples`. 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 Into>) -> Result { + let samples: Vec = samples.into(); + if samples.len() < 2 { + return Err(EvenCoreError::NotEnoughSamples { + samples: samples.len(), + }); + } + if !domain.is_finite() { + return Err(EvenCoreError::InfiniteDomain); + } + + 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(&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 sampling 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 a [`InterpolationDatum`] +/// that governs how samples are extracted relative to the stored data. +/// +/// `domain` must be a bounded interval (i.e. `domain.is_finite() == 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 { + 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`]. +/// +/// [`domain`]: UnevenCore::domain +/// [`sample_with`]: UnevenCore::sample_with +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct UnevenCore { + /// 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, + + /// The samples corresponding to the times for this curve. + /// + /// # Invariants + /// This must always have the same length as `times`. + pub samples: Vec, +} + +/// An error indicating that an [`UnevenCore`] could not be constructed. +#[derive(Debug, Error)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub enum UnevenCoreError { + /// Not enough samples were provided. + #[error("Need at least two samples to create an UnevenCore, but {samples} were provided")] + NotEnoughSamples { + /// The number of samples that were provided. + samples: usize, + }, +} + +impl UnevenCore { + /// 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. + /// + /// 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 Into>) -> Result { + let timed_samples: Vec<(f32, T)> = timed_samples.into(); + + // Filter out non-finite sample times first so they don't interfere with sorting/deduplication. + let mut timed_samples: Vec<(f32, T)> = timed_samples + .into_iter() + .filter(|(t, _)| t.is_finite()) + .collect(); + timed_samples + .sort_by(|(t0, _), (t1, _)| t0.partial_cmp(t1).unwrap_or(std::cmp::Ordering::Equal)); + timed_samples.dedup_by_key(|(t, _)| *t); + + let (times, samples): (Vec, Vec) = timed_samples.into_iter().unzip(); + + if times.len() < 2 { + return Err(UnevenCoreError::NotEnoughSamples { + samples: times.len(), + }); + } + 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(&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 sampling 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 sample times of the curve. + /// + /// [`Curve::reparametrize`]: crate::curve::Curve::reparametrize + pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenCore { + let mut timed_samples: Vec<(f32, T)> = + self.times.into_iter().map(f).zip(self.samples).collect(); + timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); + timed_samples.sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); + self.times = timed_samples.iter().map(|(t, _)| t).copied().collect(); + self.samples = timed_samples.into_iter().map(|(_, x)| x).collect(); + 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 { + /// 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, + + /// 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, +} + +/// An error that indicates that a [`ChunkedUnevenCore`] could not be formed. +#[derive(Debug, Error)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub enum ChunkedUnevenSampleCoreError { + /// 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 samples to create an UnevenCore, 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 ChunkedUnevenCore { + /// 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 entries. + /// - `values` has the incorrect length relative to `times`. + /// + /// [type-level documentation]: ChunkedUnevenCore + pub fn new( + times: impl Into>, + values: impl Into>, + width: usize, + ) -> Result { + let times: Vec = times.into(); + let values: Vec = values.into(); + + if width == 0 { + return Err(ChunkedUnevenSampleCoreError::ZeroWidth); + } + + let times = filter_sort_dedup_times(times); + + if times.len() < 2 { + return Err(ChunkedUnevenSampleCoreError::NotEnoughSamples { + samples: times.len(), + }); + } + + if values.len() != times.len() * width { + return Err(ChunkedUnevenSampleCoreError::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 sampling 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: Vec) -> Vec { + // Filter before sorting/deduplication so that NAN doesn't interfere with them. + let mut times: Vec = times.into_iter().filter(|t| t.is_finite()).collect(); + times.sort_by(|t0, t1| t0.partial_cmp(t1).unwrap()); + 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 { + 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) + } + } + } +} diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs new file mode 100644 index 0000000000000..cbc829b30f1bc --- /dev/null +++ b/crates/bevy_math/src/curve/interval.rs @@ -0,0 +1,330 @@ +//! The [`Interval`] type for nonempty intervals used by the [`Curve`](super::Curve) trait. + +use itertools::Either; +use std::{ + cmp::{max_by, min_by}, + ops::RangeInclusive, +}; +use thiserror::Error; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; +#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + +/// A nonempty closed interval, possibly infinite in either direction. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Interval { + start: f32, + end: f32, +} + +/// An error that indicates that an operation would have returned an invalid [`Interval`]. +#[derive(Debug, Error)] +#[error("The resulting interval would be invalid (empty or with a NaN endpoint)")] +pub struct InvalidIntervalError; + +/// An error indicating that an infinite interval was used where it was inappropriate. +#[derive(Debug, Error)] +#[error("This operation does not make sense in the context of an infinite interval")] +pub struct InfiniteIntervalError; + +impl Interval { + /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be infinite + /// but cannot be empty and neither endpoint can be NaN; invalid parameters will result in an error. + #[inline] + pub fn new(start: f32, end: f32) -> Result { + if start >= end || start.is_nan() || end.is_nan() { + Err(InvalidIntervalError) + } else { + Ok(Self { start, end }) + } + } + + /// Get the start of this interval. + #[inline] + pub fn start(self) -> f32 { + self.start + } + + /// Get the end of this interval. + #[inline] + pub fn end(self) -> f32 { + self.end + } + + /// Create an [`Interval`] by intersecting this interval with another. Returns an error if the + /// intersection would be empty (hence an invalid interval). + pub fn intersect(self, other: Interval) -> Result { + let lower = max_by(self.start, other.start, |x, y| x.partial_cmp(y).unwrap()); + let upper = min_by(self.end, other.end, |x, y| x.partial_cmp(y).unwrap()); + Self::new(lower, upper) + } + + /// Get the length of this interval. Note that the result may be infinite (`f32::INFINITY`). + #[inline] + pub fn length(self) -> f32 { + self.end - self.start + } + + /// Returns `true` if both endpoints of this interval are finite. + #[inline] + pub fn is_finite(self) -> bool { + self.length().is_finite() + } + + /// Returns `true` if this interval has a finite left endpoint. + #[inline] + pub fn is_left_finite(self) -> bool { + self.start.is_finite() + } + + /// Returns `true` if this interval has a finite right endpoint. + #[inline] + pub fn is_right_finite(self) -> bool { + self.end.is_finite() + } + + /// Returns `true` if `item` is contained in this interval. + #[inline] + pub fn contains(self, item: f32) -> bool { + (self.start..=self.end).contains(&item) + } + + /// Returns `true` if the other interval is contained in this interval (non-strictly). + #[inline] + pub fn contains_interval(self, other: Self) -> bool { + self.start <= other.start && self.end >= other.end + } + + /// Clamp the given `value` to lie within this interval. + #[inline] + pub fn clamp(self, value: f32) -> f32 { + value.clamp(self.start, self.end) + } + + /// Get an iterator over equally-spaced points from this interval in increasing order. + /// If `points` is 1, the start of this interval is returned. If `points` is 0, an empty + /// iterator is returned. An error is returned if the interval is unbounded. + #[inline] + pub fn spaced_points( + self, + points: usize, + ) -> Result, InfiniteIntervalError> { + if !self.is_finite() { + return Err(InfiniteIntervalError); + } + if points < 2 { + // If `points` is 1, this is `Some(self.start)` as an iterator, and if `points` is 0, + // then this is `None` as an iterator. This is written this way to avoid having to + // introduce a ternary disjunction of iterators. + let iter = (points == 1).then_some(self.start).into_iter(); + return Ok(Either::Left(iter)); + } + let step = self.length() / (points - 1) as f32; + let iter = (0..points).map(move |x| self.start + x as f32 * step); + Ok(Either::Right(iter)) + } + + /// Get the linear map which maps this curve onto the `other` one. Returns an error if either + /// interval is infinite. + #[inline] + pub(super) fn linear_map_to( + self, + other: Self, + ) -> Result f32, InfiniteIntervalError> { + if !self.is_finite() || !other.is_finite() { + return Err(InfiniteIntervalError); + } + let scale = other.length() / self.length(); + Ok(move |x| (x - self.start) * scale + other.start) + } +} + +impl TryFrom> for Interval { + type Error = InvalidIntervalError; + fn try_from(range: RangeInclusive) -> Result { + Interval::new(*range.start(), *range.end()) + } +} + +/// Create an [`Interval`] with a given `start` and `end`. Alias of [`Interval::new`]. +#[inline] +pub fn interval(start: f32, end: f32) -> Result { + Interval::new(start, end) +} + +/// The [`Interval`] from negative infinity to infinity. +#[inline] +pub fn everywhere() -> Interval { + Interval::new(f32::NEG_INFINITY, f32::INFINITY).unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + use approx::{assert_abs_diff_eq, AbsDiffEq}; + + #[test] + fn make_intervals() { + let ivl = Interval::new(2.0, -1.0); + assert!(ivl.is_err()); + + let ivl = Interval::new(-0.0, 0.0); + assert!(ivl.is_err()); + + let ivl = Interval::new(f32::NEG_INFINITY, 15.5); + assert!(ivl.is_ok()); + + let ivl = Interval::new(-2.0, f32::INFINITY); + assert!(ivl.is_ok()); + + let ivl = Interval::new(f32::NEG_INFINITY, f32::INFINITY); + assert!(ivl.is_ok()); + + let ivl = Interval::new(f32::INFINITY, f32::NEG_INFINITY); + assert!(ivl.is_err()); + + let ivl = Interval::new(-1.0, f32::NAN); + assert!(ivl.is_err()); + + let ivl = Interval::new(f32::NAN, -42.0); + assert!(ivl.is_err()); + + let ivl = Interval::new(f32::NAN, f32::NAN); + assert!(ivl.is_err()); + + let ivl = Interval::new(0.0, 1.0); + assert!(ivl.is_ok()); + } + + #[test] + fn lengths() { + let ivl = interval(-5.0, 10.0).unwrap(); + assert!((ivl.length() - 15.0).abs() <= f32::EPSILON); + + let ivl = interval(5.0, 100.0).unwrap(); + assert!((ivl.length() - 95.0).abs() <= f32::EPSILON); + + let ivl = interval(0.0, f32::INFINITY).unwrap(); + assert_eq!(ivl.length(), f32::INFINITY); + + let ivl = interval(f32::NEG_INFINITY, 0.0).unwrap(); + assert_eq!(ivl.length(), f32::INFINITY); + + let ivl = everywhere(); + assert_eq!(ivl.length(), f32::INFINITY); + } + + #[test] + fn intersections() { + let ivl1 = interval(-1.0, 1.0).unwrap(); + let ivl2 = interval(0.0, 2.0).unwrap(); + let ivl3 = interval(-3.0, 0.0).unwrap(); + let ivl4 = interval(0.0, f32::INFINITY).unwrap(); + let ivl5 = interval(f32::NEG_INFINITY, 0.0).unwrap(); + let ivl6 = everywhere(); + + assert!(ivl1 + .intersect(ivl2) + .is_ok_and(|ivl| ivl == interval(0.0, 1.0).unwrap())); + assert!(ivl1 + .intersect(ivl3) + .is_ok_and(|ivl| ivl == interval(-1.0, 0.0).unwrap())); + assert!(ivl2.intersect(ivl3).is_err()); + assert!(ivl1 + .intersect(ivl4) + .is_ok_and(|ivl| ivl == interval(0.0, 1.0).unwrap())); + assert!(ivl1 + .intersect(ivl5) + .is_ok_and(|ivl| ivl == interval(-1.0, 0.0).unwrap())); + assert!(ivl4.intersect(ivl5).is_err()); + assert_eq!(ivl1.intersect(ivl6).unwrap(), ivl1); + assert_eq!(ivl4.intersect(ivl6).unwrap(), ivl4); + assert_eq!(ivl5.intersect(ivl6).unwrap(), ivl5); + } + + #[test] + fn containment() { + let ivl = interval(0.0, 1.0).unwrap(); + assert!(ivl.contains(0.0)); + assert!(ivl.contains(1.0)); + assert!(ivl.contains(0.5)); + assert!(!ivl.contains(-0.1)); + assert!(!ivl.contains(1.1)); + assert!(!ivl.contains(f32::NAN)); + + let ivl = interval(3.0, f32::INFINITY).unwrap(); + assert!(ivl.contains(3.0)); + assert!(ivl.contains(2.0e5)); + assert!(ivl.contains(3.5e6)); + assert!(!ivl.contains(2.5)); + assert!(!ivl.contains(-1e5)); + assert!(!ivl.contains(f32::NAN)); + } + + #[test] + fn finiteness() { + assert!(!everywhere().is_finite()); + assert!(interval(0.0, 3.5e5).unwrap().is_finite()); + assert!(!interval(-2.0, f32::INFINITY).unwrap().is_finite()); + assert!(!interval(f32::NEG_INFINITY, 5.0).unwrap().is_finite()); + } + + #[test] + fn linear_maps() { + let ivl1 = interval(-3.0, 5.0).unwrap(); + let ivl2 = interval(0.0, 1.0).unwrap(); + let map = ivl1.linear_map_to(ivl2); + assert!(map.is_ok_and(|f| f(-3.0).abs_diff_eq(&0.0, f32::EPSILON) + && f(5.0).abs_diff_eq(&1.0, f32::EPSILON) + && f(1.0).abs_diff_eq(&0.5, f32::EPSILON))); + + let ivl1 = interval(0.0, 1.0).unwrap(); + let ivl2 = everywhere(); + assert!(ivl1.linear_map_to(ivl2).is_err()); + + let ivl1 = interval(f32::NEG_INFINITY, -4.0).unwrap(); + let ivl2 = interval(0.0, 1.0).unwrap(); + assert!(ivl1.linear_map_to(ivl2).is_err()); + } + + #[test] + fn spaced_points() { + let ivl = interval(0.0, 50.0).unwrap(); + let points_iter: Vec = ivl.spaced_points(1).unwrap().collect(); + assert_abs_diff_eq!(points_iter[0], 0.0); + assert_eq!(points_iter.len(), 1); + let points_iter: Vec = ivl.spaced_points(2).unwrap().collect(); + assert_abs_diff_eq!(points_iter[0], 0.0); + assert_abs_diff_eq!(points_iter[1], 50.0); + let points_iter = ivl.spaced_points(21).unwrap(); + let step = ivl.length() / 20.0; + for (index, point) in points_iter.enumerate() { + let expected = ivl.start() + step * index as f32; + assert_abs_diff_eq!(point, expected); + } + + let ivl = interval(-21.0, 79.0).unwrap(); + let points_iter = ivl.spaced_points(10000).unwrap(); + let step = ivl.length() / 9999.0; + for (index, point) in points_iter.enumerate() { + let expected = ivl.start() + step * index as f32; + assert_abs_diff_eq!(point, expected); + } + + let ivl = interval(-1.0, f32::INFINITY).unwrap(); + let points_iter = ivl.spaced_points(25); + assert!(points_iter.is_err()); + + let ivl = interval(f32::NEG_INFINITY, -25.0).unwrap(); + let points_iter = ivl.spaced_points(9); + assert!(points_iter.is_err()); + } +} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs new file mode 100644 index 0000000000000..2568dd11a3172 --- /dev/null +++ b/crates/bevy_math/src/curve/mod.rs @@ -0,0 +1,1054 @@ +//! The [`Curve`] trait, used to describe curves in a number of different domains. This module also +//! 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::{everywhere, interval, Interval}; + +use crate::StableInterpolate; +use cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError}; +use interval::{InfiniteIntervalError, InvalidIntervalError}; +use std::{marker::PhantomData, ops::Deref}; +use thiserror::Error; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; + +/// A trait for a type that can represent values of type `T` parametrized over a fixed interval. +/// Typical examples of this are actual geometric curves where `T: VectorSpace`, but other kinds +/// of interpolable data can be represented instead (or in addition). +pub trait Curve { + /// The interval over which this curve is parametrized. + fn domain(&self) -> Interval; + + /// Sample a point on this curve at the parameter value `t`, extracting the associated value. + fn sample(&self, t: f32) -> T; + + /// Sample a point on this curve at the parameter value `t`, returning `None` if the point is + /// outside of the curve's domain. + fn sample_checked(&self, t: f32) -> Option { + match self.domain().contains(t) { + true => Some(self.sample(t)), + false => None, + } + } + + /// Sample a point on this curve at the parameter value `t`, clamping `t` to lie inside the + /// domain of the curve. + fn sample_clamped(&self, t: f32) -> T { + let t = self.domain().clamp(t); + self.sample(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( + &self, + segments: usize, + interpolation: I, + ) -> Result, ResamplingError> + where + Self: Sized, + I: Fn(&T, &T, f32) -> T, + { + if segments == 0 { + return Err(ResamplingError::NotEnoughSamples(segments)); + } + if !self.domain().is_finite() { + return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); + } + + let samples: Vec = self + .domain() + .spaced_points(segments + 1) + .unwrap() + .map(|t| self.sample(t)) + .collect(); + 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 values. A total of `samples` samples are used, although at least two samples are + /// required in order to produce well-formed output. If fewer than two samples are provided, + /// or if this curve has an unbounded domain, then a [`ResamplingError`] is returned. + fn resample_auto(&self, samples: usize) -> Result, ResamplingError> + where + T: StableInterpolate, + { + if samples < 2 { + return Err(ResamplingError::NotEnoughSamples(samples)); + } + if !self.domain().is_finite() { + return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); + } + + let samples: Vec = self + .domain() + .spaced_points(samples) + .unwrap() + .map(|t| self.sample(t)) + .collect(); + 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, ResamplingError> + where + Self: Sized, + { + if samples < 2 { + return Err(ResamplingError::NotEnoughSamples(samples)); + } + if !self.domain().is_finite() { + return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); + } + + // Unwrap on `spaced_points` always succeeds because its error conditions are handled + // above. + Ok(self + .domain() + .spaced_points(samples) + .unwrap() + .map(|t| self.sample(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( + &self, + sample_times: impl IntoIterator, + interpolation: I, + ) -> Result, ResamplingError> + where + Self: Sized, + I: Fn(&T, &T, f32) -> T, + { + let mut times: Vec = sample_times + .into_iter() + .filter(|t| t.is_finite() && self.domain().contains(*t)) + .collect(); + times.dedup_by(|t1, t2| (*t1).eq(t2)); + if times.len() < 2 { + return Err(ResamplingError::NotEnoughSamples(times.len())); + } + times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); + let samples = times.iter().copied().map(|t| self.sample(t)).collect(); + Ok(UnevenSampleCurve { + core: UnevenCore { times, samples }, + interpolation, + }) + } + + /// Resample this [`Curve`] to produce a new one that is defined by 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. + fn resample_uneven_auto( + &self, + sample_times: impl IntoIterator, + ) -> Result, ResamplingError> + where + Self: Sized, + T: StableInterpolate, + { + let mut times: Vec = sample_times + .into_iter() + .filter(|t| t.is_finite() && self.domain().contains(*t)) + .collect(); + times.dedup_by(|t1, t2| (*t1).eq(t2)); + if times.len() < 2 { + return Err(ResamplingError::NotEnoughSamples(times.len())); + } + times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); + let samples = times.iter().copied().map(|t| self.sample(t)).collect(); + Ok(UnevenSampleAutoCurve { + core: UnevenCore { times, samples }, + }) + } + + /// 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)`. + fn map(self, f: F) -> MapCurve + where + Self: Sized, + F: Fn(T) -> S, + { + MapCurve { + preimage: self, + f, + _phantom: PhantomData, + } + } + + /// Create a new [`Curve`] whose parameter space is related to the parameter space of this curve + /// by `f`. For each time `t`, the sample from the new curve at time `t` is the sample from + /// this curve at time `f(t)`. The given `domain` will be the domain of the new curve. The + /// function `f` is expected to take `domain` into `self.domain()`. + /// + /// Note that this is the opposite of what one might expect intuitively; for example, if this + /// curve has a parameter interval of `[0, 1]`, then linearly mapping the parameter domain to + /// `[0, 2]` would be performed as follows, dividing by what might be perceived as the scaling + /// factor rather than multiplying: + /// ``` + /// # use bevy_math::curve::*; + /// let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// let domain = my_curve.domain(); + /// let scaled_curve = my_curve.reparametrize(interval(0.0, 2.0).unwrap(), |t| t / 2.0); + /// ``` + /// This kind of linear remapping is provided by the convenience method + /// [`Curve::reparametrize_linear`], which requires only the desired domain for the new curve. + /// + /// # Examples + /// ``` + /// // Reverse a curve: + /// # use bevy_math::curve::*; + /// # use bevy_math::vec2; + /// let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// let domain = my_curve.domain(); + /// let reversed_curve = my_curve.reparametrize(domain, |t| domain.end() - t); + /// + /// // Take a segment of a curve: + /// # let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// let curve_segment = my_curve.reparametrize(interval(0.0, 0.5).unwrap(), |t| 0.5 + t); + /// + /// // Reparametrize by an easing curve: + /// # let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// # let easing_curve = constant_curve(interval(0.0, 1.0).unwrap(), vec2(1.0, 1.0)); + /// let domain = my_curve.domain(); + /// let eased_curve = my_curve.reparametrize(domain, |t| easing_curve.sample(t).y); + /// ``` + fn reparametrize(self, domain: Interval, f: F) -> ReparamCurve + where + Self: Sized, + F: Fn(f32) -> f32, + { + ReparamCurve { + domain, + base: self, + f, + _phantom: PhantomData, + } + } + + /// Linearly reparametrize this [`Curve`], producing a new curve whose domain is the given + /// `domain` instead of the current one. This operation is only valid for curves with finite + /// domains; if either this curve's domain or the given `domain` is infinite, an + /// [`InfiniteIntervalError`] is returned. + fn reparametrize_linear( + self, + domain: Interval, + ) -> Result, InfiniteIntervalError> + where + Self: Sized, + { + if !domain.is_finite() { + return Err(InfiniteIntervalError); + } + + Ok(LinearReparamCurve { + base: self, + new_domain: domain, + _phantom: PhantomData, + }) + } + + /// Reparametrize this [`Curve`] by sampling from another curve. + /// + /// TODO: Figure out what the right signature for this is; currently, this is less flexible than + /// just using `C`, because `&C` is a curve anyway, but this version probably footguns less. + fn reparametrize_by_curve(self, other: &C) -> CurveReparamCurve + where + Self: Sized, + C: Curve, + { + CurveReparamCurve { + base: self, + reparam_curve: other, + _phantom: PhantomData, + } + } + + /// Create a new [`Curve`] which is the graph of this one; that is, its output includes the + /// parameter itself in the samples. For example, if this curve outputs `x` at time `t`, then + /// the produced curve will produce `(t, x)` at time `t`. + fn graph(self) -> GraphCurve + where + Self: Sized, + { + GraphCurve { + base: self, + _phantom: PhantomData, + } + } + + /// Create a new [`Curve`] by zipping this curve together with another. The sample at time `t` + /// in the new curve is `(x, y)`, where `x` is the sample of `self` at time `t` and `y` is the + /// sample of `other` at time `t`. The domain of the new curve is the intersection of the + /// domains of its constituents. If the domain intersection would be empty, an + /// [`InvalidIntervalError`] is returned. + fn zip(self, other: C) -> Result, InvalidIntervalError> + where + Self: Sized, + C: Curve + Sized, + { + let domain = self.domain().intersect(other.domain())?; + Ok(ProductCurve { + domain, + first: self, + second: other, + _phantom: PhantomData, + }) + } + + /// Create a new [`Curve`] by composing this curve end-to-end with another, producing another curve + /// with outputs of the same type. The domain of the other curve is translated so that its start + /// coincides with where this curve ends. A [`ChainError`] is returned if this curve's domain + /// doesn't have a finite right endpoint or if `other`'s domain doesn't have a finite left endpoint. + fn chain(self, other: C) -> Result, ChainError> + where + Self: Sized, + C: Curve, + { + if !self.domain().is_right_finite() { + return Err(ChainError::RightInfiniteFirst); + } + if !other.domain().is_left_finite() { + return Err(ChainError::LeftInfiniteSecond); + } + Ok(ChainCurve { + first: self, + second: other, + _phantom: PhantomData, + }) + } + + /// 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 + /// ``` + /// # 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 + /// // ownership of its input. + /// let samples = my_curve.by_ref().map(|x| x * 2.0).resample_auto(100).unwrap(); + /// // Do something else with `my_curve` since we retained ownership: + /// let new_curve = my_curve.reparametrize_linear(interval(-1.0, 1.0).unwrap()).unwrap(); + /// ``` + fn by_ref(&self) -> &Self + where + Self: Sized, + { + self + } + + /// Flip this curve so that its tuple output is arranged the other way. + fn flip(self) -> impl Curve<(V, U)> + where + Self: Sized + Curve<(U, V)>, + { + self.map(|(u, v)| (v, u)) + } +} + +impl Curve for D +where + C: Curve + ?Sized, + D: Deref, +{ + fn domain(&self) -> Interval { + >::domain(self) + } + + fn sample(&self, t: f32) -> T { + >::sample(self, t) + } +} + +/// 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 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")] + InfiniteInterval(InfiniteIntervalError), +} + +/// An error indicating that an end-to-end composition couldn't be performed because of +/// malformed inputs. +#[derive(Debug, Error)] +#[error("Could not compose these curves together")] +pub enum ChainError { + /// The right endpoint of the first curve was infinite. + #[error("The first curve has an infinite right endpoint")] + RightInfiniteFirst, + + /// The left endpoint of the second curve was infinite. + #[error("The second curve has an infinite left endpoint")] + LeftInfiniteSecond, +} + +/// A curve which takes a constant value over its domain. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct ConstantCurve { + domain: Interval, + value: T, +} + +impl Curve for ConstantCurve +where + T: Clone, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample(&self, _t: f32) -> T { + self.value.clone() + } +} + +/// A curve defined by a function. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct FunctionCurve { + domain: Interval, + f: F, + _phantom: PhantomData, +} + +impl Curve for FunctionCurve +where + F: Fn(f32) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample(&self, t: f32) -> T { + (self.f)(t) + } +} + +/// 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 { + core: EvenCore, + interpolation: I, +} + +impl Curve for SampleCurve +where + T: Clone, + I: Fn(&T, &T, f32) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample(&self, t: f32) -> T { + self.core.sample_with(t, &self.interpolation) + } +} + +impl SampleCurve { + /// 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 Into>, + interpolation: I, + ) -> Result + 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 { + core: EvenCore, +} + +impl Curve for SampleAutoCurve +where + T: StableInterpolate, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample(&self, t: f32) -> T { + self.core + .sample_with(t, ::interpolate_stable) + } +} + +impl SampleAutoCurve { + /// 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 Into>) -> Result { + 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 { + core: UnevenCore, + interpolation: I, +} + +impl Curve for UnevenSampleCurve +where + T: Clone, + I: Fn(&T, &T, f32) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample(&self, t: f32) -> T { + self.core.sample_with(t, &self.interpolation) + } +} + +impl UnevenSampleCurve { + /// 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 Into>, + interpolation: I, + ) -> Result { + 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 { + 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 { + core: UnevenCore, +} + +impl Curve for UnevenSampleAutoCurve +where + T: StableInterpolate, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample(&self, t: f32) -> T { + self.core + .sample_with(t, ::interpolate_stable) + } +} + +impl UnevenSampleAutoCurve { + /// 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 Into>) -> Result { + 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 { + Self { + core: self.core.map_sample_times(f), + } + } +} + +/// A curve whose samples are defined by mapping samples from another curve through a +/// given function. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct MapCurve { + preimage: C, + f: F, + _phantom: PhantomData<(S, T)>, +} + +impl Curve for MapCurve +where + C: Curve, + F: Fn(S) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.preimage.domain() + } + + #[inline] + fn sample(&self, t: f32) -> T { + (self.f)(self.preimage.sample(t)) + } +} + +/// A curve whose sample space is mapped onto that of some base curve's before sampling. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct ReparamCurve { + domain: Interval, + base: C, + f: F, + _phantom: PhantomData, +} + +impl Curve for ReparamCurve +where + C: Curve, + F: Fn(f32) -> f32, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample(&self, t: f32) -> T { + self.base.sample((self.f)(t)) + } +} + +/// A curve that has had its domain altered by a linear remapping. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct LinearReparamCurve { + base: C, + /// Invariants: This interval must always be bounded. + new_domain: Interval, + _phantom: PhantomData, +} + +impl Curve for LinearReparamCurve +where + C: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.new_domain + } + + #[inline] + fn sample(&self, t: f32) -> T { + let f = self.new_domain.linear_map_to(self.base.domain()).unwrap(); + self.base.sample(f(t)) + } +} + +/// A curve that has been reparametrized by another curve, using that curve to transform the +/// sample times before sampling. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct CurveReparamCurve { + base: C, + reparam_curve: D, + _phantom: PhantomData, +} + +impl Curve for CurveReparamCurve +where + C: Curve, + D: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.reparam_curve.domain() + } + + #[inline] + fn sample(&self, t: f32) -> T { + let sample_time = self.reparam_curve.sample(t); + self.base.sample(sample_time) + } +} + +/// A curve that is the graph of another curve over its parameter space. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct GraphCurve { + base: C, + _phantom: PhantomData, +} + +impl Curve<(f32, T)> for GraphCurve +where + C: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.base.domain() + } + + #[inline] + fn sample(&self, t: f32) -> (f32, T) { + (t, self.base.sample(t)) + } +} + +/// A curve that combines the data from two constituent curves into a tuple output type. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct ProductCurve { + domain: Interval, + first: C, + second: D, + _phantom: PhantomData<(S, T)>, +} + +impl Curve<(S, T)> for ProductCurve +where + C: Curve, + D: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample(&self, t: f32) -> (S, T) { + (self.first.sample(t), self.second.sample(t)) + } +} + +/// The curve that results from chaining one curve with another. The second curve is +/// effectively reparametrized so that its start is at the end of the first. +/// +/// For this to be well-formed, the first curve's domain must be right-finite and the second's +/// must be left-finite. +pub struct ChainCurve { + first: C, + second: D, + _phantom: PhantomData, +} + +impl Curve for ChainCurve +where + C: Curve, + D: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + // This unwrap always succeeds because `first` has a valid Interval as its domain and the + // length of `second` cannot be NAN. It's still fine if it's infinity. + Interval::new( + self.first.domain().start(), + self.first.domain().end() + self.second.domain().length(), + ) + .unwrap() + } + + #[inline] + fn sample(&self, t: f32) -> T { + if t > self.first.domain().end() { + self.second.sample( + // `t - first.domain.end` computes the offset into the domain of the second. + t - self.first.domain().end() + self.second.domain().start(), + ) + } else { + self.first.sample(t) + } + } +} + +/// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. +pub fn constant_curve(domain: Interval, value: T) -> ConstantCurve { + ConstantCurve { domain, value } +} + +/// Convert the given function `f` into a [`Curve`] with the given `domain`, sampled by +/// evaluating the function. +pub fn function_curve(domain: Interval, f: F) -> FunctionCurve +where + F: Fn(f32) -> T, +{ + FunctionCurve { + domain, + f, + _phantom: PhantomData, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Quat; + use approx::{assert_abs_diff_eq, AbsDiffEq}; + use std::f32::consts::TAU; + + #[test] + fn constant_curves() { + let curve = constant_curve(everywhere(), 5.0); + assert!(curve.sample(-35.0) == 5.0); + + let curve = constant_curve(interval(0.0, 1.0).unwrap(), true); + assert!(curve.sample(2.0)); + assert!(curve.sample_checked(2.0).is_none()); + } + + #[test] + fn function_curves() { + let curve = function_curve(everywhere(), |t| t * t); + assert!(curve.sample(2.0).abs_diff_eq(&4.0, f32::EPSILON)); + assert!(curve.sample(-3.0).abs_diff_eq(&9.0, f32::EPSILON)); + + let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), f32::log2); + assert_eq!(curve.sample(3.5), f32::log2(3.5)); + assert!(curve.sample(-1.0).is_nan()); + assert!(curve.sample_checked(-1.0).is_none()); + } + + #[test] + fn mapping() { + let curve = function_curve(everywhere(), |t| t * 3.0 + 1.0); + let mapped_curve = curve.map(|x| x / 7.0); + assert_eq!(mapped_curve.sample(3.5), (3.5 * 3.0 + 1.0) / 7.0); + assert_eq!(mapped_curve.sample(-1.0), (-1.0 * 3.0 + 1.0) / 7.0); + assert_eq!(mapped_curve.domain(), everywhere()); + + let curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t * TAU); + let mapped_curve = curve.map(Quat::from_rotation_z); + assert_eq!(mapped_curve.sample(0.0), Quat::IDENTITY); + assert!(mapped_curve.sample(1.0).is_near_identity()); + assert_eq!(mapped_curve.domain(), interval(0.0, 1.0).unwrap()); + } + + #[test] + fn reparametrization() { + let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), f32::log2); + let reparametrized_curve = curve + .by_ref() + .reparametrize(interval(0.0, f32::INFINITY).unwrap(), f32::exp2); + assert_abs_diff_eq!(reparametrized_curve.sample(3.5), 3.5); + assert_abs_diff_eq!(reparametrized_curve.sample(100.0), 100.0); + assert_eq!( + reparametrized_curve.domain(), + interval(0.0, f32::INFINITY).unwrap() + ); + + let reparametrized_curve = curve + .by_ref() + .reparametrize(interval(0.0, 1.0).unwrap(), |t| t + 1.0); + assert_abs_diff_eq!(reparametrized_curve.sample(0.0), 0.0); + assert_abs_diff_eq!(reparametrized_curve.sample(1.0), 1.0); + assert_eq!(reparametrized_curve.domain(), interval(0.0, 1.0).unwrap()); + } + + #[test] + fn multiple_maps() { + // Make sure these actually happen in the right order. + let curve = function_curve(interval(0.0, 1.0).unwrap(), f32::exp2); + let first_mapped = curve.map(f32::log2); + let second_mapped = first_mapped.map(|x| x * -2.0); + assert_abs_diff_eq!(second_mapped.sample(0.0), 0.0); + assert_abs_diff_eq!(second_mapped.sample(0.5), -1.0); + assert_abs_diff_eq!(second_mapped.sample(1.0), -2.0); + } + + #[test] + fn multiple_reparams() { + // Make sure these happen in the right order too. + let curve = function_curve(interval(0.0, 1.0).unwrap(), f32::exp2); + let first_reparam = curve.reparametrize(interval(1.0, 2.0).unwrap(), f32::log2); + let second_reparam = first_reparam.reparametrize(interval(0.0, 1.0).unwrap(), |t| t + 1.0); + assert_abs_diff_eq!(second_reparam.sample(0.0), 1.0); + assert_abs_diff_eq!(second_reparam.sample(0.5), 1.5); + assert_abs_diff_eq!(second_reparam.sample(1.0), 2.0); + } + + #[test] + fn resampling() { + let curve = function_curve(interval(1.0, 4.0).unwrap(), f32::log2); + + // Need at least two points to sample. + let nice_try = curve.by_ref().resample_auto(1); + 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(101).unwrap(); + let step = curve.domain().length() / 100.0; + for index in 0..101 { + let test_pt = curve.domain().start() + index as f32 * step; + let expected = curve.sample(test_pt); + assert_abs_diff_eq!(resampled_curve.sample(test_pt), expected, epsilon = 1e-6); + } + + // Another example. + let curve = function_curve(interval(0.0, TAU).unwrap(), f32::cos); + let resampled_curve = curve.by_ref().resample_auto(1001).unwrap(); + let step = curve.domain().length() / 1000.0; + for index in 0..1001 { + let test_pt = curve.domain().start() + index as f32 * step; + let expected = curve.sample(test_pt); + assert_abs_diff_eq!(resampled_curve.sample(test_pt), expected, epsilon = 1e-6); + } + } + + #[test] + fn uneven_resampling() { + let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), f32::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(test_pt); + assert_eq!(resampled_curve.sample(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(), f32::log2); + let sample_points = (0..10).map(|idx| (idx as f32).exp2()); + let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap(); + for idx in 0..10 { + let test_pt = (idx as f32).exp2(); + let expected = curve.sample(test_pt); + assert_eq!(resampled_curve.sample(test_pt), expected); + } + assert_abs_diff_eq!(resampled_curve.domain().start(), 1.0); + assert_abs_diff_eq!(resampled_curve.domain().end(), 512.0); + } +} diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 03726f5693706..f34e452fce334 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -17,6 +17,7 @@ pub mod bounding; pub mod common_traits; mod compass; pub mod cubic_splines; +pub mod curve; mod direction; mod float_ord; mod isometry; From 48a5ecc4ff5c1cf1f6cb59404be13e794cef443e Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 5 Aug 2024 09:17:39 -0400 Subject: [PATCH 02/22] Chop of resampling to be reviewed later --- crates/bevy_math/src/curve/cores.rs | 554 ---------------------------- crates/bevy_math/src/curve/mod.rs | 446 +--------------------- 2 files changed, 1 insertion(+), 999 deletions(-) delete mode 100644 crates/bevy_math/src/curve/cores.rs diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs deleted file mode 100644 index 30574362489b9..0000000000000 --- a/crates/bevy_math/src/curve/cores.rs +++ /dev/null @@ -1,554 +0,0 @@ -//! Core data structures to be used internally in Curve implementations, encapsulating storage -//! and access patterns for reuse. -//! -//! The `Core` types here expose their fields publically so that it is easier to extend them, -//! but in doing so, you must maintain the invariants of those fields yourself. The provided -//! methods all maintain the invariants, so this only concerns you if you manually mutate them. - -use super::interval::Interval; -use core::fmt::Debug; -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 { - /// 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 InterpolationDatum { - /// 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(self, f: impl Fn(T) -> S) -> InterpolationDatum { - 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. -/// -/// [the provided constructor]: EvenCore::new -/// [`domain`]: EvenCore::domain -/// [`sample_with`]: EvenCore::sample_with -/// -/// # Example -/// ```rust -/// # use bevy_math::curve::*; -/// # use bevy_math::curve::cores::*; -/// enum InterpolationMode { -/// Linear, -/// Step, -/// } -/// -/// trait LinearInterpolate { -/// fn lerp(&self, other: &Self, t: f32) -> Self; -/// } -/// -/// fn step(first: &T, second: &T, t: f32) -> T { -/// if t >= 1.0 { -/// second.clone() -/// } else { -/// first.clone() -/// } -/// } -/// -/// struct MyCurve { -/// core: EvenCore, -/// interpolation_mode: InterpolationMode, -/// } -/// -/// impl Curve for MyCurve -/// where -/// T: LinearInterpolate + Clone, -/// { -/// fn domain(&self) -> Interval { -/// self.core.domain() -/// } -/// -/// fn sample(&self, t: f32) -> T { -/// match self.interpolation_mode { -/// InterpolationMode::Linear => self.core.sample_with(t, ::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 { - /// 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, -} - -/// An error indicating that a [`EvenCore`] could not be constructed. -#[derive(Debug, Error, PartialEq, Eq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub enum EvenCoreError { - /// Not enough samples were provided. - #[error("Need at least two samples to create a 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 a domain with an infinite endpoint")] - InfiniteDomain, -} - -impl EvenCore { - /// Create a new [`EvenCore`] from the specified `domain` and `samples`. 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 Into>) -> Result { - let samples: Vec = samples.into(); - if samples.len() < 2 { - return Err(EvenCoreError::NotEnoughSamples { - samples: samples.len(), - }); - } - if !domain.is_finite() { - return Err(EvenCoreError::InfiniteDomain); - } - - 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(&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 sampling 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 a [`InterpolationDatum`] -/// that governs how samples are extracted relative to the stored data. -/// -/// `domain` must be a bounded interval (i.e. `domain.is_finite() == 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 { - 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`]. -/// -/// [`domain`]: UnevenCore::domain -/// [`sample_with`]: UnevenCore::sample_with -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct UnevenCore { - /// 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, - - /// The samples corresponding to the times for this curve. - /// - /// # Invariants - /// This must always have the same length as `times`. - pub samples: Vec, -} - -/// An error indicating that an [`UnevenCore`] could not be constructed. -#[derive(Debug, Error)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub enum UnevenCoreError { - /// Not enough samples were provided. - #[error("Need at least two samples to create an UnevenCore, but {samples} were provided")] - NotEnoughSamples { - /// The number of samples that were provided. - samples: usize, - }, -} - -impl UnevenCore { - /// 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. - /// - /// 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 Into>) -> Result { - let timed_samples: Vec<(f32, T)> = timed_samples.into(); - - // Filter out non-finite sample times first so they don't interfere with sorting/deduplication. - let mut timed_samples: Vec<(f32, T)> = timed_samples - .into_iter() - .filter(|(t, _)| t.is_finite()) - .collect(); - timed_samples - .sort_by(|(t0, _), (t1, _)| t0.partial_cmp(t1).unwrap_or(std::cmp::Ordering::Equal)); - timed_samples.dedup_by_key(|(t, _)| *t); - - let (times, samples): (Vec, Vec) = timed_samples.into_iter().unzip(); - - if times.len() < 2 { - return Err(UnevenCoreError::NotEnoughSamples { - samples: times.len(), - }); - } - 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(&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 sampling 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 sample times of the curve. - /// - /// [`Curve::reparametrize`]: crate::curve::Curve::reparametrize - pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenCore { - let mut timed_samples: Vec<(f32, T)> = - self.times.into_iter().map(f).zip(self.samples).collect(); - timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); - timed_samples.sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); - self.times = timed_samples.iter().map(|(t, _)| t).copied().collect(); - self.samples = timed_samples.into_iter().map(|(_, x)| x).collect(); - 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 { - /// 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, - - /// 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, -} - -/// An error that indicates that a [`ChunkedUnevenCore`] could not be formed. -#[derive(Debug, Error)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub enum ChunkedUnevenSampleCoreError { - /// 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 samples to create an UnevenCore, 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 ChunkedUnevenCore { - /// 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 entries. - /// - `values` has the incorrect length relative to `times`. - /// - /// [type-level documentation]: ChunkedUnevenCore - pub fn new( - times: impl Into>, - values: impl Into>, - width: usize, - ) -> Result { - let times: Vec = times.into(); - let values: Vec = values.into(); - - if width == 0 { - return Err(ChunkedUnevenSampleCoreError::ZeroWidth); - } - - let times = filter_sort_dedup_times(times); - - if times.len() < 2 { - return Err(ChunkedUnevenSampleCoreError::NotEnoughSamples { - samples: times.len(), - }); - } - - if values.len() != times.len() * width { - return Err(ChunkedUnevenSampleCoreError::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 sampling 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: Vec) -> Vec { - // Filter before sorting/deduplication so that NAN doesn't interfere with them. - let mut times: Vec = times.into_iter().filter(|t| t.is_finite()).collect(); - times.sort_by(|t0, t1| t0.partial_cmp(t1).unwrap()); - 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 { - 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) - } - } - } -} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 2568dd11a3172..4c0ccb70d6360 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -2,13 +2,10 @@ //! 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::{everywhere, interval, Interval}; -use crate::StableInterpolate; -use cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError}; use interval::{InfiniteIntervalError, InvalidIntervalError}; use std::{marker::PhantomData, ops::Deref}; use thiserror::Error; @@ -42,180 +39,6 @@ pub trait Curve { self.sample(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( - &self, - segments: usize, - interpolation: I, - ) -> Result, ResamplingError> - where - Self: Sized, - I: Fn(&T, &T, f32) -> T, - { - if segments == 0 { - return Err(ResamplingError::NotEnoughSamples(segments)); - } - if !self.domain().is_finite() { - return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); - } - - let samples: Vec = self - .domain() - .spaced_points(segments + 1) - .unwrap() - .map(|t| self.sample(t)) - .collect(); - 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 values. A total of `samples` samples are used, although at least two samples are - /// required in order to produce well-formed output. If fewer than two samples are provided, - /// or if this curve has an unbounded domain, then a [`ResamplingError`] is returned. - fn resample_auto(&self, samples: usize) -> Result, ResamplingError> - where - T: StableInterpolate, - { - if samples < 2 { - return Err(ResamplingError::NotEnoughSamples(samples)); - } - if !self.domain().is_finite() { - return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); - } - - let samples: Vec = self - .domain() - .spaced_points(samples) - .unwrap() - .map(|t| self.sample(t)) - .collect(); - 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, ResamplingError> - where - Self: Sized, - { - if samples < 2 { - return Err(ResamplingError::NotEnoughSamples(samples)); - } - if !self.domain().is_finite() { - return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); - } - - // Unwrap on `spaced_points` always succeeds because its error conditions are handled - // above. - Ok(self - .domain() - .spaced_points(samples) - .unwrap() - .map(|t| self.sample(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( - &self, - sample_times: impl IntoIterator, - interpolation: I, - ) -> Result, ResamplingError> - where - Self: Sized, - I: Fn(&T, &T, f32) -> T, - { - let mut times: Vec = sample_times - .into_iter() - .filter(|t| t.is_finite() && self.domain().contains(*t)) - .collect(); - times.dedup_by(|t1, t2| (*t1).eq(t2)); - if times.len() < 2 { - return Err(ResamplingError::NotEnoughSamples(times.len())); - } - times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); - let samples = times.iter().copied().map(|t| self.sample(t)).collect(); - Ok(UnevenSampleCurve { - core: UnevenCore { times, samples }, - interpolation, - }) - } - - /// Resample this [`Curve`] to produce a new one that is defined by 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. - fn resample_uneven_auto( - &self, - sample_times: impl IntoIterator, - ) -> Result, ResamplingError> - where - Self: Sized, - T: StableInterpolate, - { - let mut times: Vec = sample_times - .into_iter() - .filter(|t| t.is_finite() && self.domain().contains(*t)) - .collect(); - times.dedup_by(|t1, t2| (*t1).eq(t2)); - if times.len() < 2 { - return Err(ResamplingError::NotEnoughSamples(times.len())); - } - times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); - let samples = times.iter().copied().map(|t| self.sample(t)).collect(); - Ok(UnevenSampleAutoCurve { - core: UnevenCore { times, samples }, - }) - } - /// 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)`. @@ -378,7 +201,7 @@ pub trait Curve { /// 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 @@ -417,20 +240,6 @@ where } } -/// 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 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")] - InfiniteInterval(InfiniteIntervalError), -} - /// An error indicating that an end-to-end composition couldn't be performed because of /// malformed inputs. #[derive(Debug, Error)] @@ -494,196 +303,6 @@ 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 { - core: EvenCore, - interpolation: I, -} - -impl Curve for SampleCurve -where - T: Clone, - I: Fn(&T, &T, f32) -> T, -{ - #[inline] - fn domain(&self) -> Interval { - self.core.domain() - } - - #[inline] - fn sample(&self, t: f32) -> T { - self.core.sample_with(t, &self.interpolation) - } -} - -impl SampleCurve { - /// 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 Into>, - interpolation: I, - ) -> Result - 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 { - core: EvenCore, -} - -impl Curve for SampleAutoCurve -where - T: StableInterpolate, -{ - #[inline] - fn domain(&self) -> Interval { - self.core.domain() - } - - #[inline] - fn sample(&self, t: f32) -> T { - self.core - .sample_with(t, ::interpolate_stable) - } -} - -impl SampleAutoCurve { - /// 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 Into>) -> Result { - 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 { - core: UnevenCore, - interpolation: I, -} - -impl Curve for UnevenSampleCurve -where - T: Clone, - I: Fn(&T, &T, f32) -> T, -{ - #[inline] - fn domain(&self) -> Interval { - self.core.domain() - } - - #[inline] - fn sample(&self, t: f32) -> T { - self.core.sample_with(t, &self.interpolation) - } -} - -impl UnevenSampleCurve { - /// 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 Into>, - interpolation: I, - ) -> Result { - 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 { - 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 { - core: UnevenCore, -} - -impl Curve for UnevenSampleAutoCurve -where - T: StableInterpolate, -{ - #[inline] - fn domain(&self) -> Interval { - self.core.domain() - } - - #[inline] - fn sample(&self, t: f32) -> T { - self.core - .sample_with(t, ::interpolate_stable) - } -} - -impl UnevenSampleAutoCurve { - /// 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 Into>) -> Result { - 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 { - Self { - core: self.core.map_sample_times(f), - } - } -} - /// A curve whose samples are defined by mapping samples from another curve through a /// given function. #[derive(Clone, Debug)] @@ -988,67 +607,4 @@ mod tests { assert_abs_diff_eq!(second_reparam.sample(0.5), 1.5); assert_abs_diff_eq!(second_reparam.sample(1.0), 2.0); } - - #[test] - fn resampling() { - let curve = function_curve(interval(1.0, 4.0).unwrap(), f32::log2); - - // Need at least two points to sample. - let nice_try = curve.by_ref().resample_auto(1); - 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(101).unwrap(); - let step = curve.domain().length() / 100.0; - for index in 0..101 { - let test_pt = curve.domain().start() + index as f32 * step; - let expected = curve.sample(test_pt); - assert_abs_diff_eq!(resampled_curve.sample(test_pt), expected, epsilon = 1e-6); - } - - // Another example. - let curve = function_curve(interval(0.0, TAU).unwrap(), f32::cos); - let resampled_curve = curve.by_ref().resample_auto(1001).unwrap(); - let step = curve.domain().length() / 1000.0; - for index in 0..1001 { - let test_pt = curve.domain().start() + index as f32 * step; - let expected = curve.sample(test_pt); - assert_abs_diff_eq!(resampled_curve.sample(test_pt), expected, epsilon = 1e-6); - } - } - - #[test] - fn uneven_resampling() { - let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), f32::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(test_pt); - assert_eq!(resampled_curve.sample(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(), f32::log2); - let sample_points = (0..10).map(|idx| (idx as f32).exp2()); - let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap(); - for idx in 0..10 { - let test_pt = (idx as f32).exp2(); - let expected = curve.sample(test_pt); - assert_eq!(resampled_curve.sample(test_pt), expected); - } - assert_abs_diff_eq!(resampled_curve.domain().start(), 1.0); - assert_abs_diff_eq!(resampled_curve.domain().end(), 512.0); - } } From 5d90b70091c420192e98240788226d5b427b513d Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 6 Aug 2024 16:34:05 -0400 Subject: [PATCH 03/22] Some doc and naming updates from code review --- crates/bevy_math/src/curve/interval.rs | 15 ++-- crates/bevy_math/src/curve/mod.rs | 97 ++++++++++++++++++-------- 2 files changed, 78 insertions(+), 34 deletions(-) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index cbc829b30f1bc..28817750e012f 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -37,7 +37,8 @@ pub struct InfiniteIntervalError; impl Interval { /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be infinite - /// but cannot be empty and neither endpoint can be NaN; invalid parameters will result in an error. + /// but cannot be empty (so `start` must be less than `end`) and neither endpoint can be NaN; invalid + /// parameters will result in an error. #[inline] pub fn new(start: f32, end: f32) -> Result { if start >= end || start.is_nan() || end.is_nan() { @@ -79,15 +80,15 @@ impl Interval { self.length().is_finite() } - /// Returns `true` if this interval has a finite left endpoint. + /// Returns `true` if this interval has a finite start. #[inline] - pub fn is_left_finite(self) -> bool { + pub fn has_finite_start(self) -> bool { self.start.is_finite() } - /// Returns `true` if this interval has a finite right endpoint. + /// Returns `true` if this interval has a finite end. #[inline] - pub fn is_right_finite(self) -> bool { + pub fn has_finite_end(self) -> bool { self.end.is_finite() } @@ -97,7 +98,9 @@ impl Interval { (self.start..=self.end).contains(&item) } - /// Returns `true` if the other interval is contained in this interval (non-strictly). + /// Returns `true` if the other interval is contained in this interval. + /// + /// This is non-strict: each interval will contain itself. #[inline] pub fn contains_interval(self, other: Self) -> bool { self.start <= other.start && self.end >= other.end diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 4c0ccb70d6360..726d322a5d9e2 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -60,13 +60,12 @@ pub trait Curve { /// function `f` is expected to take `domain` into `self.domain()`. /// /// Note that this is the opposite of what one might expect intuitively; for example, if this - /// curve has a parameter interval of `[0, 1]`, then linearly mapping the parameter domain to + /// curve has a parameter domain of `[0, 1]`, then stretching the parameter domain to /// `[0, 2]` would be performed as follows, dividing by what might be perceived as the scaling /// factor rather than multiplying: /// ``` /// # use bevy_math::curve::*; /// let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); - /// let domain = my_curve.domain(); /// let scaled_curve = my_curve.reparametrize(interval(0.0, 2.0).unwrap(), |t| t / 2.0); /// ``` /// This kind of linear remapping is provided by the convenience method @@ -128,9 +127,10 @@ pub trait Curve { /// Reparametrize this [`Curve`] by sampling from another curve. /// - /// TODO: Figure out what the right signature for this is; currently, this is less flexible than - /// just using `C`, because `&C` is a curve anyway, but this version probably footguns less. - fn reparametrize_by_curve(self, other: &C) -> CurveReparamCurve + /// 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`. + fn reparametrize_by_curve(self, other: C) -> CurveReparamCurve where Self: Sized, C: Curve, @@ -142,9 +142,12 @@ pub trait Curve { } } - /// Create a new [`Curve`] which is the graph of this one; that is, its output includes the - /// parameter itself in the samples. For example, if this curve outputs `x` at time `t`, then - /// the produced curve will produce `(t, x)` at time `t`. + /// Create a new [`Curve`] which is the graph of this one; that is, its output echoes the sample + /// time as part of a tuple. + /// + /// 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`, the output of this method + /// is a `Curve<(f32, T)>`. fn graph(self) -> GraphCurve where Self: Sized, @@ -155,11 +158,12 @@ pub trait Curve { } } - /// Create a new [`Curve`] by zipping this curve together with another. The sample at time `t` - /// in the new curve is `(x, y)`, where `x` is the sample of `self` at time `t` and `y` is the - /// sample of `other` at time `t`. The domain of the new curve is the intersection of the - /// domains of its constituents. If the domain intersection would be empty, an - /// [`InvalidIntervalError`] is returned. + /// Create a new [`Curve`] by zipping this curve together with another. + /// + /// The sample at time `t` in the new curve is `(x, y)`, where `x` is the sample of `self` at + /// time `t` and `y` is the sample of `other` at time `t`. The domain of the new curve is the + /// intersection of the domains of its constituents. If the domain intersection would be empty, + /// an [`InvalidIntervalError`] is returned. fn zip(self, other: C) -> Result, InvalidIntervalError> where Self: Sized, @@ -183,11 +187,11 @@ pub trait Curve { Self: Sized, C: Curve, { - if !self.domain().is_right_finite() { - return Err(ChainError::RightInfiniteFirst); + if !self.domain().has_finite_end() { + return Err(ChainError::FirstEndInfinite); } - if !other.domain().is_left_finite() { - return Err(ChainError::LeftInfiniteSecond); + if !other.domain().has_finite_start() { + return Err(ChainError::SecondStartInfinite); } Ok(ChainCurve { first: self, @@ -246,15 +250,17 @@ where #[error("Could not compose these curves together")] pub enum ChainError { /// The right endpoint of the first curve was infinite. - #[error("The first curve has an infinite right endpoint")] - RightInfiniteFirst, + #[error("The first curve's domain has an infinite end")] + FirstEndInfinite, /// The left endpoint of the second curve was infinite. - #[error("The second curve has an infinite left endpoint")] - LeftInfiniteSecond, + #[error("The second curve's domain has an infinite start")] + SecondStartInfinite, } -/// A curve which takes a constant value over its domain. +/// 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. #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] @@ -263,6 +269,17 @@ pub struct ConstantCurve { value: T, } +impl ConstantCurve +where + T: Clone, +{ + /// Create a constant curve, which has the given `domain` and always produces the given `value` + /// when sampled. + pub fn new(domain: Interval, value: T) -> Self { + Self { domain, value } + } +} + impl Curve for ConstantCurve where T: Clone, @@ -278,7 +295,10 @@ where } } -/// A curve defined by a function. +/// A curve defined by a function together with a fixed domain. +/// +/// This is a curve that holds an inner function `f` which takes numbers (`f32`) as input and produces +/// output of type `T`. The value of this curve when sampled at time `t` is just `f(t)`. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] @@ -288,6 +308,21 @@ pub struct FunctionCurve { _phantom: PhantomData, } +impl FunctionCurve +where + F: Fn(f32) -> T, +{ + /// Create a new curve with the given `domain` from the given `function`. When sampled, the + /// `function` is evaluated at the sample time to compute the output. + pub fn new(domain: Interval, function: F) -> Self { + FunctionCurve { + domain, + f: function, + _phantom: PhantomData, + } + } +} + impl Curve for FunctionCurve where F: Fn(f32) -> T, @@ -304,7 +339,7 @@ where } /// A curve whose samples are defined by mapping samples from another curve through a -/// given function. +/// given function. Curves of this type are produced by [`Curve::map`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] @@ -331,6 +366,7 @@ where } /// A curve whose sample space is mapped onto that of some base curve's before sampling. +/// Curves of this type are produced by [`Curve::reparametrize`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] @@ -357,7 +393,8 @@ where } } -/// A curve that has had its domain altered by a linear remapping. +/// A curve that has had its domain changed by a linear reparametrization (stretching and scaling). +/// Curves of this type are produced by [`Curve::reparametrize_linear`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] @@ -385,7 +422,7 @@ where } /// A curve that has been reparametrized by another curve, using that curve to transform the -/// sample times before sampling. +/// sample times before sampling. Curves of this type are produced by [`Curve::reparametrize_by_curve`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] @@ -412,7 +449,8 @@ where } } -/// A curve that is the graph of another curve over its parameter space. +/// A curve that is the graph of another curve over its parameter space. Curves of this type are +/// produced by [`Curve::graph`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] @@ -436,7 +474,8 @@ where } } -/// A curve that combines the data from two constituent curves into a tuple output type. +/// A curve that combines the output data from two constituent curves into a tuple output. Curves +/// of this type are produced by [`Curve::zip`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] @@ -468,6 +507,8 @@ where /// /// For this to be well-formed, the first curve's domain must be right-finite and the second's /// must be left-finite. +/// +/// Curves of this type are produced by [`Curve::chain`]. pub struct ChainCurve { first: C, second: D, From bbc12ab16036f66cfb06c9f9b52eec71c33ebbd9 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 6 Aug 2024 16:59:19 -0400 Subject: [PATCH 04/22] Rename sample -> sample_unchecked, sample_checked -> sample --- crates/bevy_math/src/curve/mod.rs | 108 +++++++++++++++++------------- 1 file changed, 61 insertions(+), 47 deletions(-) diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 726d322a5d9e2..75bef44b6f8cc 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -18,16 +18,24 @@ use bevy_reflect::Reflect; /// of interpolable data can be represented instead (or in addition). pub trait Curve { /// The interval over which this curve is parametrized. + /// + /// This is the range of values of `t` where we can sample the curve and receive valid output. fn domain(&self) -> Interval; /// Sample a point on this curve at the parameter value `t`, extracting the associated value. - fn sample(&self, t: f32) -> T; + /// This is the unchecked version of sampling, which should only be used if the sample time `t` + /// is already known to lie within the curve's domain. + /// + /// Values sampled from outside of a curve's domain are generally considered invalid; data which + /// is nonsensical or otherwise useless may be returned in such a circumstance, and extrapolation + /// beyond a curve's domain should not be relied upon. + fn sample_unchecked(&self, t: f32) -> T; /// Sample a point on this curve at the parameter value `t`, returning `None` if the point is /// outside of the curve's domain. - fn sample_checked(&self, t: f32) -> Option { + fn sample(&self, t: f32) -> Option { match self.domain().contains(t) { - true => Some(self.sample(t)), + true => Some(self.sample_unchecked(t)), false => None, } } @@ -36,7 +44,7 @@ pub trait Curve { /// domain of the curve. fn sample_clamped(&self, t: f32) -> T { let t = self.domain().clamp(t); - self.sample(t) + self.sample_unchecked(t) } /// Create a new curve by mapping the values of this curve via a function `f`; i.e., if the @@ -88,7 +96,7 @@ pub trait Curve { /// # let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); /// # let easing_curve = constant_curve(interval(0.0, 1.0).unwrap(), vec2(1.0, 1.0)); /// let domain = my_curve.domain(); - /// let eased_curve = my_curve.reparametrize(domain, |t| easing_curve.sample(t).y); + /// let eased_curve = my_curve.reparametrize(domain, |t| easing_curve.sample_unchecked(t).y); /// ``` fn reparametrize(self, domain: Interval, f: F) -> ReparamCurve where @@ -239,8 +247,8 @@ where >::domain(self) } - fn sample(&self, t: f32) -> T { - >::sample(self, t) + fn sample_unchecked(&self, t: f32) -> T { + >::sample_unchecked(self, t) } } @@ -290,7 +298,7 @@ where } #[inline] - fn sample(&self, _t: f32) -> T { + fn sample_unchecked(&self, _t: f32) -> T { self.value.clone() } } @@ -333,7 +341,7 @@ where } #[inline] - fn sample(&self, t: f32) -> T { + fn sample_unchecked(&self, t: f32) -> T { (self.f)(t) } } @@ -360,8 +368,8 @@ where } #[inline] - fn sample(&self, t: f32) -> T { - (self.f)(self.preimage.sample(t)) + fn sample_unchecked(&self, t: f32) -> T { + (self.f)(self.preimage.sample_unchecked(t)) } } @@ -388,8 +396,8 @@ where } #[inline] - fn sample(&self, t: f32) -> T { - self.base.sample((self.f)(t)) + fn sample_unchecked(&self, t: f32) -> T { + self.base.sample_unchecked((self.f)(t)) } } @@ -415,9 +423,9 @@ where } #[inline] - fn sample(&self, t: f32) -> T { + fn sample_unchecked(&self, t: f32) -> T { let f = self.new_domain.linear_map_to(self.base.domain()).unwrap(); - self.base.sample(f(t)) + self.base.sample_unchecked(f(t)) } } @@ -443,9 +451,9 @@ where } #[inline] - fn sample(&self, t: f32) -> T { - let sample_time = self.reparam_curve.sample(t); - self.base.sample(sample_time) + fn sample_unchecked(&self, t: f32) -> T { + let sample_time = self.reparam_curve.sample_unchecked(t); + self.base.sample_unchecked(sample_time) } } @@ -469,8 +477,8 @@ where } #[inline] - fn sample(&self, t: f32) -> (f32, T) { - (t, self.base.sample(t)) + fn sample_unchecked(&self, t: f32) -> (f32, T) { + (t, self.base.sample_unchecked(t)) } } @@ -497,8 +505,11 @@ where } #[inline] - fn sample(&self, t: f32) -> (S, T) { - (self.first.sample(t), self.second.sample(t)) + fn sample_unchecked(&self, t: f32) -> (S, T) { + ( + self.first.sample_unchecked(t), + self.second.sample_unchecked(t), + ) } } @@ -532,14 +543,14 @@ where } #[inline] - fn sample(&self, t: f32) -> T { + fn sample_unchecked(&self, t: f32) -> T { if t > self.first.domain().end() { - self.second.sample( + self.second.sample_unchecked( // `t - first.domain.end` computes the offset into the domain of the second. t - self.first.domain().end() + self.second.domain().start(), ) } else { - self.first.sample(t) + self.first.sample_unchecked(t) } } } @@ -572,37 +583,40 @@ mod tests { #[test] fn constant_curves() { let curve = constant_curve(everywhere(), 5.0); - assert!(curve.sample(-35.0) == 5.0); + assert!(curve.sample_unchecked(-35.0) == 5.0); let curve = constant_curve(interval(0.0, 1.0).unwrap(), true); - assert!(curve.sample(2.0)); - assert!(curve.sample_checked(2.0).is_none()); + assert!(curve.sample_unchecked(2.0)); + assert!(curve.sample(2.0).is_none()); } #[test] fn function_curves() { let curve = function_curve(everywhere(), |t| t * t); - assert!(curve.sample(2.0).abs_diff_eq(&4.0, f32::EPSILON)); - assert!(curve.sample(-3.0).abs_diff_eq(&9.0, f32::EPSILON)); + assert!(curve.sample_unchecked(2.0).abs_diff_eq(&4.0, f32::EPSILON)); + assert!(curve.sample_unchecked(-3.0).abs_diff_eq(&9.0, f32::EPSILON)); let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), f32::log2); - assert_eq!(curve.sample(3.5), f32::log2(3.5)); - assert!(curve.sample(-1.0).is_nan()); - assert!(curve.sample_checked(-1.0).is_none()); + assert_eq!(curve.sample_unchecked(3.5), f32::log2(3.5)); + assert!(curve.sample_unchecked(-1.0).is_nan()); + assert!(curve.sample(-1.0).is_none()); } #[test] fn mapping() { let curve = function_curve(everywhere(), |t| t * 3.0 + 1.0); let mapped_curve = curve.map(|x| x / 7.0); - assert_eq!(mapped_curve.sample(3.5), (3.5 * 3.0 + 1.0) / 7.0); - assert_eq!(mapped_curve.sample(-1.0), (-1.0 * 3.0 + 1.0) / 7.0); + assert_eq!(mapped_curve.sample_unchecked(3.5), (3.5 * 3.0 + 1.0) / 7.0); + assert_eq!( + mapped_curve.sample_unchecked(-1.0), + (-1.0 * 3.0 + 1.0) / 7.0 + ); assert_eq!(mapped_curve.domain(), everywhere()); let curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t * TAU); let mapped_curve = curve.map(Quat::from_rotation_z); - assert_eq!(mapped_curve.sample(0.0), Quat::IDENTITY); - assert!(mapped_curve.sample(1.0).is_near_identity()); + assert_eq!(mapped_curve.sample_unchecked(0.0), Quat::IDENTITY); + assert!(mapped_curve.sample_unchecked(1.0).is_near_identity()); assert_eq!(mapped_curve.domain(), interval(0.0, 1.0).unwrap()); } @@ -612,8 +626,8 @@ mod tests { let reparametrized_curve = curve .by_ref() .reparametrize(interval(0.0, f32::INFINITY).unwrap(), f32::exp2); - assert_abs_diff_eq!(reparametrized_curve.sample(3.5), 3.5); - assert_abs_diff_eq!(reparametrized_curve.sample(100.0), 100.0); + assert_abs_diff_eq!(reparametrized_curve.sample_unchecked(3.5), 3.5); + assert_abs_diff_eq!(reparametrized_curve.sample_unchecked(100.0), 100.0); assert_eq!( reparametrized_curve.domain(), interval(0.0, f32::INFINITY).unwrap() @@ -622,8 +636,8 @@ mod tests { let reparametrized_curve = curve .by_ref() .reparametrize(interval(0.0, 1.0).unwrap(), |t| t + 1.0); - assert_abs_diff_eq!(reparametrized_curve.sample(0.0), 0.0); - assert_abs_diff_eq!(reparametrized_curve.sample(1.0), 1.0); + assert_abs_diff_eq!(reparametrized_curve.sample_unchecked(0.0), 0.0); + assert_abs_diff_eq!(reparametrized_curve.sample_unchecked(1.0), 1.0); assert_eq!(reparametrized_curve.domain(), interval(0.0, 1.0).unwrap()); } @@ -633,9 +647,9 @@ mod tests { let curve = function_curve(interval(0.0, 1.0).unwrap(), f32::exp2); let first_mapped = curve.map(f32::log2); let second_mapped = first_mapped.map(|x| x * -2.0); - assert_abs_diff_eq!(second_mapped.sample(0.0), 0.0); - assert_abs_diff_eq!(second_mapped.sample(0.5), -1.0); - assert_abs_diff_eq!(second_mapped.sample(1.0), -2.0); + assert_abs_diff_eq!(second_mapped.sample_unchecked(0.0), 0.0); + assert_abs_diff_eq!(second_mapped.sample_unchecked(0.5), -1.0); + assert_abs_diff_eq!(second_mapped.sample_unchecked(1.0), -2.0); } #[test] @@ -644,8 +658,8 @@ mod tests { let curve = function_curve(interval(0.0, 1.0).unwrap(), f32::exp2); let first_reparam = curve.reparametrize(interval(1.0, 2.0).unwrap(), f32::log2); let second_reparam = first_reparam.reparametrize(interval(0.0, 1.0).unwrap(), |t| t + 1.0); - assert_abs_diff_eq!(second_reparam.sample(0.0), 1.0); - assert_abs_diff_eq!(second_reparam.sample(0.5), 1.5); - assert_abs_diff_eq!(second_reparam.sample(1.0), 2.0); + assert_abs_diff_eq!(second_reparam.sample_unchecked(0.0), 1.0); + assert_abs_diff_eq!(second_reparam.sample_unchecked(0.5), 1.5); + assert_abs_diff_eq!(second_reparam.sample_unchecked(1.0), 2.0); } } From c5dfa74ff933fa56c1cf5ab1b3b52455bbe444c0 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 6 Aug 2024 17:20:51 -0400 Subject: [PATCH 05/22] everywhere() -> Interval::EVERYWHERE constant --- crates/bevy_math/src/curve/interval.rs | 13 +++++++------ crates/bevy_math/src/curve/mod.rs | 10 +++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index 28817750e012f..7cb5712bab702 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -3,6 +3,7 @@ use itertools::Either; use std::{ cmp::{max_by, min_by}, + f32::{INFINITY, NEG_INFINITY}, ops::RangeInclusive, }; use thiserror::Error; @@ -48,6 +49,12 @@ impl Interval { } } + /// An interval which stretches across the entire real line from negative infinity to infinity. + pub const EVERYWHERE: Self = Self { + start: NEG_INFINITY, + end: INFINITY, + }; + /// Get the start of this interval. #[inline] pub fn start(self) -> f32 { @@ -163,12 +170,6 @@ pub fn interval(start: f32, end: f32) -> Result Interval::new(start, end) } -/// The [`Interval`] from negative infinity to infinity. -#[inline] -pub fn everywhere() -> Interval { - Interval::new(f32::NEG_INFINITY, f32::INFINITY).unwrap() -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 75bef44b6f8cc..c495630cbc22a 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -4,7 +4,7 @@ pub mod interval; -pub use interval::{everywhere, interval, Interval}; +pub use interval::{interval, Interval}; use interval::{InfiniteIntervalError, InvalidIntervalError}; use std::{marker::PhantomData, ops::Deref}; @@ -582,7 +582,7 @@ mod tests { #[test] fn constant_curves() { - let curve = constant_curve(everywhere(), 5.0); + let curve = constant_curve(Interval::EVERYWHERE, 5.0); assert!(curve.sample_unchecked(-35.0) == 5.0); let curve = constant_curve(interval(0.0, 1.0).unwrap(), true); @@ -592,7 +592,7 @@ mod tests { #[test] fn function_curves() { - let curve = function_curve(everywhere(), |t| t * t); + let curve = function_curve(Interval::EVERYWHERE, |t| t * t); assert!(curve.sample_unchecked(2.0).abs_diff_eq(&4.0, f32::EPSILON)); assert!(curve.sample_unchecked(-3.0).abs_diff_eq(&9.0, f32::EPSILON)); @@ -604,14 +604,14 @@ mod tests { #[test] fn mapping() { - let curve = function_curve(everywhere(), |t| t * 3.0 + 1.0); + let curve = function_curve(Interval::EVERYWHERE, |t| t * 3.0 + 1.0); let mapped_curve = curve.map(|x| x / 7.0); assert_eq!(mapped_curve.sample_unchecked(3.5), (3.5 * 3.0 + 1.0) / 7.0); assert_eq!( mapped_curve.sample_unchecked(-1.0), (-1.0 * 3.0 + 1.0) / 7.0 ); - assert_eq!(mapped_curve.domain(), everywhere()); + assert_eq!(mapped_curve.domain(), Interval::EVERYWHERE); let curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t * TAU); let mapped_curve = curve.map(Quat::from_rotation_z); From c220e28a769cd619b6004ba9fbacb8b821faf3c8 Mon Sep 17 00:00:00 2001 From: Matty Date: Tue, 6 Aug 2024 17:25:07 -0400 Subject: [PATCH 06/22] Apply doc changes from code review Co-authored-by: Alice Cecile --- crates/bevy_math/src/curve/interval.rs | 2 +- crates/bevy_math/src/curve/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index 7cb5712bab702..5d9ef0da71eee 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -142,7 +142,7 @@ impl Interval { Ok(Either::Right(iter)) } - /// Get the linear map which maps this curve onto the `other` one. Returns an error if either + /// Get the linear function which maps this interval onto the `other` one. Returns an error if either /// interval is infinite. #[inline] pub(super) fn linear_map_to( diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index c495630cbc22a..afcc1f1b1bcfb 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -15,7 +15,7 @@ use bevy_reflect::Reflect; /// A trait for a type that can represent values of type `T` parametrized over a fixed interval. /// Typical examples of this are actual geometric curves where `T: VectorSpace`, but other kinds -/// of interpolable data can be represented instead (or in addition). +/// of interpolable data can be represented as well. pub trait Curve { /// The interval over which this curve is parametrized. /// From b599f1b02069c9d78287870d3cf50d5da338b3e5 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 6 Aug 2024 19:32:00 -0400 Subject: [PATCH 07/22] Fix CI issues --- crates/bevy_math/src/curve/interval.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index 5d9ef0da71eee..97b7dc85b7054 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -3,7 +3,6 @@ use itertools::Either; use std::{ cmp::{max_by, min_by}, - f32::{INFINITY, NEG_INFINITY}, ops::RangeInclusive, }; use thiserror::Error; @@ -51,8 +50,8 @@ impl Interval { /// An interval which stretches across the entire real line from negative infinity to infinity. pub const EVERYWHERE: Self = Self { - start: NEG_INFINITY, - end: INFINITY, + start: f32::NEG_INFINITY, + end: f32::INFINITY, }; /// Get the start of this interval. @@ -222,7 +221,7 @@ mod tests { let ivl = interval(f32::NEG_INFINITY, 0.0).unwrap(); assert_eq!(ivl.length(), f32::INFINITY); - let ivl = everywhere(); + let ivl = Interval::EVERYWHERE; assert_eq!(ivl.length(), f32::INFINITY); } @@ -233,7 +232,7 @@ mod tests { let ivl3 = interval(-3.0, 0.0).unwrap(); let ivl4 = interval(0.0, f32::INFINITY).unwrap(); let ivl5 = interval(f32::NEG_INFINITY, 0.0).unwrap(); - let ivl6 = everywhere(); + let ivl6 = Interval::EVERYWHERE; assert!(ivl1 .intersect(ivl2) @@ -275,7 +274,7 @@ mod tests { #[test] fn finiteness() { - assert!(!everywhere().is_finite()); + assert!(!Interval::EVERYWHERE.is_finite()); assert!(interval(0.0, 3.5e5).unwrap().is_finite()); assert!(!interval(-2.0, f32::INFINITY).unwrap().is_finite()); assert!(!interval(f32::NEG_INFINITY, 5.0).unwrap().is_finite()); @@ -291,7 +290,7 @@ mod tests { && f(1.0).abs_diff_eq(&0.5, f32::EPSILON))); let ivl1 = interval(0.0, 1.0).unwrap(); - let ivl2 = everywhere(); + let ivl2 = Interval::EVERYWHERE; assert!(ivl1.linear_map_to(ivl2).is_err()); let ivl1 = interval(f32::NEG_INFINITY, -4.0).unwrap(); From e4a95f92892e8b19728f08b8096c08def0495b9a Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 7 Aug 2024 08:31:51 -0400 Subject: [PATCH 08/22] Make Interval::intersect use total_cmp --- crates/bevy_math/src/curve/interval.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index 97b7dc85b7054..e7a74e09f813e 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -69,8 +69,8 @@ impl Interval { /// Create an [`Interval`] by intersecting this interval with another. Returns an error if the /// intersection would be empty (hence an invalid interval). pub fn intersect(self, other: Interval) -> Result { - let lower = max_by(self.start, other.start, |x, y| x.partial_cmp(y).unwrap()); - let upper = min_by(self.end, other.end, |x, y| x.partial_cmp(y).unwrap()); + let lower = max_by(self.start, other.start, |x, y| x.total_cmp(y)); + let upper = min_by(self.end, other.end, |x, y| x.total_cmp(y)); Self::new(lower, upper) } From efa56da33b8c66fcc6d6a92b81f5ed18f41000e4 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 7 Aug 2024 15:35:37 -0400 Subject: [PATCH 09/22] Rename infinite -> unbounded consistently, custom error type for Curve::reparametrize_linear --- crates/bevy_math/src/curve/interval.rs | 43 ++++++++++++++------------ crates/bevy_math/src/curve/mod.rs | 36 +++++++++++++++------ 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index e7a74e09f813e..d18e6be7c71c9 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -12,7 +12,10 @@ use bevy_reflect::Reflect; #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -/// A nonempty closed interval, possibly infinite in either direction. +/// A nonempty closed interval, possibly unbounded in either direction. +/// +/// In other words, the interval may stretch all the way to positive or negative infinity, but it +/// will always have some nonempty interior. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] @@ -30,13 +33,13 @@ pub struct Interval { #[error("The resulting interval would be invalid (empty or with a NaN endpoint)")] pub struct InvalidIntervalError; -/// An error indicating that an infinite interval was used where it was inappropriate. +/// An error indicating that an unbounded interval was used where it was inappropriate. #[derive(Debug, Error)] -#[error("This operation does not make sense in the context of an infinite interval")] -pub struct InfiniteIntervalError; +#[error("This operation does not make sense in the context of an unbounded interval")] +pub struct UnboundedIntervalError; impl Interval { - /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be infinite + /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be unbounded /// but cannot be empty (so `start` must be less than `end`) and neither endpoint can be NaN; invalid /// parameters will result in an error. #[inline] @@ -80,9 +83,11 @@ impl Interval { self.end - self.start } - /// Returns `true` if both endpoints of this interval are finite. + /// Returns `true` if this interval is bounded — that is, if both its start and end are finite. + /// + /// Equivalently, an interval is bounded if its length is finite. #[inline] - pub fn is_finite(self) -> bool { + pub fn is_bounded(self) -> bool { self.length().is_finite() } @@ -125,9 +130,9 @@ impl Interval { pub fn spaced_points( self, points: usize, - ) -> Result, InfiniteIntervalError> { - if !self.is_finite() { - return Err(InfiniteIntervalError); + ) -> Result, UnboundedIntervalError> { + if !self.is_bounded() { + return Err(UnboundedIntervalError); } if points < 2 { // If `points` is 1, this is `Some(self.start)` as an iterator, and if `points` is 0, @@ -142,14 +147,14 @@ impl Interval { } /// Get the linear function which maps this interval onto the `other` one. Returns an error if either - /// interval is infinite. + /// interval is unbounded. #[inline] pub(super) fn linear_map_to( self, other: Self, - ) -> Result f32, InfiniteIntervalError> { - if !self.is_finite() || !other.is_finite() { - return Err(InfiniteIntervalError); + ) -> Result f32, UnboundedIntervalError> { + if !self.is_bounded() || !other.is_bounded() { + return Err(UnboundedIntervalError); } let scale = other.length() / self.length(); Ok(move |x| (x - self.start) * scale + other.start) @@ -273,11 +278,11 @@ mod tests { } #[test] - fn finiteness() { - assert!(!Interval::EVERYWHERE.is_finite()); - assert!(interval(0.0, 3.5e5).unwrap().is_finite()); - assert!(!interval(-2.0, f32::INFINITY).unwrap().is_finite()); - assert!(!interval(f32::NEG_INFINITY, 5.0).unwrap().is_finite()); + fn boundedness() { + assert!(!Interval::EVERYWHERE.is_bounded()); + assert!(interval(0.0, 3.5e5).unwrap().is_bounded()); + assert!(!interval(-2.0, f32::INFINITY).unwrap().is_bounded()); + assert!(!interval(f32::NEG_INFINITY, 5.0).unwrap().is_bounded()); } #[test] diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index afcc1f1b1bcfb..ba79833bcae3f 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -6,7 +6,7 @@ pub mod interval; pub use interval::{interval, Interval}; -use interval::{InfiniteIntervalError, InvalidIntervalError}; +use interval::InvalidIntervalError; use std::{marker::PhantomData, ops::Deref}; use thiserror::Error; @@ -112,18 +112,22 @@ pub trait Curve { } /// Linearly reparametrize this [`Curve`], producing a new curve whose domain is the given - /// `domain` instead of the current one. This operation is only valid for curves with finite - /// domains; if either this curve's domain or the given `domain` is infinite, an - /// [`InfiniteIntervalError`] is returned. + /// `domain` instead of the current one. This operation is only valid for curves with bounded + /// domains; if either this curve's domain or the given `domain` is unbounded, an error is + /// returned. fn reparametrize_linear( self, domain: Interval, - ) -> Result, InfiniteIntervalError> + ) -> Result, LinearReparamError> where Self: Sized, { - if !domain.is_finite() { - return Err(InfiniteIntervalError); + if !self.domain().is_bounded() { + return Err(LinearReparamError::SourceCurveUnbounded); + } + + if !domain.is_bounded() { + return Err(LinearReparamError::TargetIntervalUnbounded); } Ok(LinearReparamCurve { @@ -171,7 +175,7 @@ pub trait Curve { /// The sample at time `t` in the new curve is `(x, y)`, where `x` is the sample of `self` at /// time `t` and `y` is the sample of `other` at time `t`. The domain of the new curve is the /// intersection of the domains of its constituents. If the domain intersection would be empty, - /// an [`InvalidIntervalError`] is returned. + /// an error is returned. fn zip(self, other: C) -> Result, InvalidIntervalError> where Self: Sized, @@ -189,7 +193,7 @@ pub trait Curve { /// Create a new [`Curve`] by composing this curve end-to-end with another, producing another curve /// with outputs of the same type. The domain of the other curve is translated so that its start /// coincides with where this curve ends. A [`ChainError`] is returned if this curve's domain - /// doesn't have a finite right endpoint or if `other`'s domain doesn't have a finite left endpoint. + /// doesn't have a finite end or if `other`'s domain doesn't have a finite start. fn chain(self, other: C) -> Result, ChainError> where Self: Sized, @@ -252,6 +256,20 @@ where } } +/// An error indicating that a linear reparametrization couldn't be performed because of +/// malformed inputs. +#[derive(Debug, Error)] +#[error("Could not build a linear function to reparametrize this curve")] +pub enum LinearReparamError { + /// The source curve that was to be reparametrized had unbounded domain. + #[error("This curve has unbounded domain")] + SourceCurveUnbounded, + + /// The target interval for reparametrization was unbounded. + #[error("The target interval for reparametrization is unbounded")] + TargetIntervalUnbounded, +} + /// An error indicating that an end-to-end composition couldn't be performed because of /// malformed inputs. #[derive(Debug, Error)] From 6ecdf61da20333f6a10609466ae9038875c88b61 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 7 Aug 2024 15:47:55 -0400 Subject: [PATCH 10/22] Add interval containment tests --- crates/bevy_math/src/curve/interval.rs | 19 +++++++++++++++++++ crates/bevy_math/src/curve/mod.rs | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index d18e6be7c71c9..c66fc78e9d71d 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -277,6 +277,25 @@ mod tests { assert!(!ivl.contains(f32::NAN)); } + #[test] + fn interval_containment() { + let ivl = interval(0.0, 1.0).unwrap(); + assert!(ivl.contains_interval(interval(-0.0, 0.5).unwrap())); + assert!(ivl.contains_interval(interval(0.5, 1.0).unwrap())); + assert!(ivl.contains_interval(interval(0.25, 0.75).unwrap())); + assert!(!ivl.contains_interval(interval(-0.25, 0.5).unwrap())); + assert!(!ivl.contains_interval(interval(0.5, 1.25).unwrap())); + assert!(!ivl.contains_interval(interval(0.25, f32::INFINITY).unwrap())); + assert!(!ivl.contains_interval(interval(f32::NEG_INFINITY, 0.75).unwrap())); + + let big_ivl = interval(0.0, f32::INFINITY).unwrap(); + assert!(big_ivl.contains_interval(interval(0.0, 5.0).unwrap())); + assert!(big_ivl.contains_interval(interval(0.0, f32::INFINITY).unwrap())); + assert!(big_ivl.contains_interval(interval(1.0, 5.0).unwrap())); + assert!(!big_ivl.contains_interval(interval(-1.0, f32::INFINITY).unwrap())); + assert!(!big_ivl.contains_interval(interval(-2.0, 5.0).unwrap())); + } + #[test] fn boundedness() { assert!(!Interval::EVERYWHERE.is_bounded()); diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index ba79833bcae3f..cd9f9976c80c4 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -15,7 +15,7 @@ use bevy_reflect::Reflect; /// A trait for a type that can represent values of type `T` parametrized over a fixed interval. /// Typical examples of this are actual geometric curves where `T: VectorSpace`, but other kinds -/// of interpolable data can be represented as well. +/// of output data can be represented as well. pub trait Curve { /// The interval over which this curve is parametrized. /// From 69aa4bdab408c367f432083ffea59de721677759 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 7 Aug 2024 16:17:13 -0400 Subject: [PATCH 11/22] Make Interval unboundedness errors more granular --- crates/bevy_math/src/curve/interval.rs | 38 ++++++++++++++++++-------- crates/bevy_math/src/curve/mod.rs | 2 ++ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index c66fc78e9d71d..a96eae1943b2a 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -33,10 +33,24 @@ pub struct Interval { #[error("The resulting interval would be invalid (empty or with a NaN endpoint)")] pub struct InvalidIntervalError; -/// An error indicating that an unbounded interval was used where it was inappropriate. +/// An error indicating that spaced points could not be extracted from an unbounded interval. #[derive(Debug, Error)] -#[error("This operation does not make sense in the context of an unbounded interval")] -pub struct UnboundedIntervalError; +#[error("Cannot extract spaced points from an unbounded interval")] +pub struct SpacedPointsError; + +/// An error indicating that a linear map between intervals could not be constructed because of +/// unboundedness. +#[derive(Debug, Error)] +#[error("Could not construct linear function to map between intervals")] +pub(super) enum LinearMapError { + /// The source interval being mapped out of was unbounded. + #[error("The source interval is unbounded")] + SourceUnbounded, + + /// The target interval being mapped into was unbounded. + #[error("The target interval is unbounded")] + TargetUnbounded, +} impl Interval { /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be unbounded @@ -130,9 +144,9 @@ impl Interval { pub fn spaced_points( self, points: usize, - ) -> Result, UnboundedIntervalError> { + ) -> Result, SpacedPointsError> { if !self.is_bounded() { - return Err(UnboundedIntervalError); + return Err(SpacedPointsError); } if points < 2 { // If `points` is 1, this is `Some(self.start)` as an iterator, and if `points` is 0, @@ -149,13 +163,15 @@ impl Interval { /// Get the linear function which maps this interval onto the `other` one. Returns an error if either /// interval is unbounded. #[inline] - pub(super) fn linear_map_to( - self, - other: Self, - ) -> Result f32, UnboundedIntervalError> { - if !self.is_bounded() || !other.is_bounded() { - return Err(UnboundedIntervalError); + pub(super) fn linear_map_to(self, other: Self) -> Result f32, LinearMapError> { + if !self.is_bounded() { + return Err(LinearMapError::SourceUnbounded); + } + + if !other.is_bounded() { + return Err(LinearMapError::TargetUnbounded); } + let scale = other.length() / self.length(); Ok(move |x| (x - self.start) * scale + other.start) } diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index cd9f9976c80c4..30d5fee9b33af 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -425,6 +425,7 @@ where #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct LinearReparamCurve { + /// Invariants: The domain of this curve must always be bounded. base: C, /// Invariants: This interval must always be bounded. new_domain: Interval, @@ -442,6 +443,7 @@ where #[inline] fn sample_unchecked(&self, t: f32) -> T { + // The invariants imply this unwrap always succeeds. let f = self.new_domain.linear_map_to(self.base.domain()).unwrap(); self.base.sample_unchecked(f(t)) } From 44c84b6726e16dfe3b08bcdfb2ec5f7bf7060d4d Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 7 Aug 2024 16:22:14 -0400 Subject: [PATCH 12/22] Lint --- crates/bevy_math/src/curve/interval.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index a96eae1943b2a..dd263b3e685c6 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -86,8 +86,8 @@ impl Interval { /// Create an [`Interval`] by intersecting this interval with another. Returns an error if the /// intersection would be empty (hence an invalid interval). pub fn intersect(self, other: Interval) -> Result { - let lower = max_by(self.start, other.start, |x, y| x.total_cmp(y)); - let upper = min_by(self.end, other.end, |x, y| x.total_cmp(y)); + let lower = max_by(self.start, other.start, f32::total_cmp); + let upper = min_by(self.end, other.end, f32::total_cmp); Self::new(lower, upper) } From d295abee72eb951ab46a92f5e568ddab39bb65e0 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 10 Aug 2024 17:14:43 -0400 Subject: [PATCH 13/22] Initial commit for Part 2 --- crates/bevy_math/src/curve/cores.rs | 555 ++++++++++++++++++++++++++++ crates/bevy_math/src/curve/mod.rs | 462 ++++++++++++++++++++++- 2 files changed, 1016 insertions(+), 1 deletion(-) create mode 100644 crates/bevy_math/src/curve/cores.rs diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs new file mode 100644 index 0000000000000..a08f2d514bf51 --- /dev/null +++ b/crates/bevy_math/src/curve/cores.rs @@ -0,0 +1,555 @@ +//! Core data structures to be used internally in Curve implementations, encapsulating storage +//! and access patterns for reuse. +//! +//! The `Core` types here expose their fields publically 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 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 { + /// 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 InterpolationDatum { + /// 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(self, f: impl Fn(T) -> S) -> InterpolationDatum { + 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::*; +/// enum InterpolationMode { +/// Linear, +/// Step, +/// } +/// +/// trait LinearInterpolate { +/// fn lerp(&self, other: &Self, t: f32) -> Self; +/// } +/// +/// fn step(first: &T, second: &T, t: f32) -> T { +/// if t >= 1.0 { +/// second.clone() +/// } else { +/// first.clone() +/// } +/// } +/// +/// struct MyCurve { +/// core: EvenCore, +/// interpolation_mode: InterpolationMode, +/// } +/// +/// impl Curve for MyCurve +/// where +/// T: LinearInterpolate + Clone, +/// { +/// fn domain(&self) -> Interval { +/// self.core.domain() +/// } +/// +/// fn sample_unchecked(&self, t: f32) -> T { +/// match self.interpolation_mode { +/// InterpolationMode::Linear => self.core.sample_with(t, ::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 { + /// 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, +} + +/// 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 EvenCore { + /// Create a new [`EvenCore`] from the specified `domain` and `samples`. 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 Into>) -> Result { + let samples: Vec = samples.into(); + 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(&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 { + 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`]. +/// +/// [`domain`]: UnevenCore::domain +/// [`sample_with`]: UnevenCore::sample_with +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct UnevenCore { + /// 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, + + /// The samples corresponding to the times for this curve. + /// + /// # Invariants + /// This must always have the same length as `times`. + pub samples: Vec, +} + +/// 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 samples to create an UnevenCore, but {samples} were provided")] + NotEnoughSamples { + /// The number of samples that were provided. + samples: usize, + }, +} + +impl UnevenCore { + /// 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. + /// + /// 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 Into>) -> Result { + let timed_samples: Vec<(f32, T)> = timed_samples.into(); + + // Filter out non-finite sample times first so they don't interfere with sorting/deduplication. + let mut timed_samples: Vec<(f32, T)> = timed_samples + .into_iter() + .filter(|(t, _)| t.is_finite()) + .collect(); + timed_samples + .sort_by(|(t0, _), (t1, _)| t0.partial_cmp(t1).unwrap_or(std::cmp::Ordering::Equal)); + timed_samples.dedup_by_key(|(t, _)| *t); + + let (times, samples): (Vec, Vec) = timed_samples.into_iter().unzip(); + + if times.len() < 2 { + return Err(UnevenCoreError::NotEnoughSamples { + samples: times.len(), + }); + } + 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(&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 + pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenCore { + let mut timed_samples: Vec<(f32, T)> = + self.times.into_iter().map(f).zip(self.samples).collect(); + timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); + timed_samples.sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); + self.times = timed_samples.iter().map(|(t, _)| t).copied().collect(); + self.samples = timed_samples.into_iter().map(|(_, x)| x).collect(); + 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 { + /// 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, + + /// 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, +} + +/// 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 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 ChunkedUnevenCore { + /// 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 entries. + /// - `values` has the incorrect length relative to `times`. + /// + /// [type-level documentation]: ChunkedUnevenCore + pub fn new( + times: impl Into>, + values: impl Into>, + width: usize, + ) -> Result { + let times: Vec = times.into(); + let values: Vec = 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: Vec) -> Vec { + // Filter before sorting/deduplication so that NAN doesn't interfere with them. + let mut times: Vec = times.into_iter().filter(|t| t.is_finite()).collect(); + times.sort_by(|t0, t1| t0.partial_cmp(t1).unwrap()); + 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 { + 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) + } + } + } +} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 30d5fee9b33af..f3acaa0f0c1eb 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -2,10 +2,13 @@ //! 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 crate::StableInterpolate; +use cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError}; use interval::InvalidIntervalError; use std::{marker::PhantomData, ops::Deref}; use thiserror::Error; @@ -212,12 +215,194 @@ pub trait Curve { }) } + /// 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( + &self, + segments: usize, + interpolation: I, + ) -> Result, ResamplingError> + where + Self: Sized, + I: Fn(&T, &T, f32) -> T, + { + if segments == 0 { + return Err(ResamplingError::NotEnoughSamples(segments)); + } + if !self.domain().is_bounded() { + return Err(ResamplingError::UnboundedDomain); + } + + let samples: Vec = self + .domain() + .spaced_points(segments + 1) + .unwrap() + .map(|t| self.sample_unchecked(t)) + .collect(); + 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 values. A total of `samples` samples are used, although at least two samples are + /// required in order to produce well-formed output. If fewer than two samples are provided, + /// or if this curve has an unbounded domain, then a [`ResamplingError`] is returned. + fn resample_auto(&self, samples: usize) -> Result, ResamplingError> + where + T: StableInterpolate, + { + if samples < 2 { + return Err(ResamplingError::NotEnoughSamples(samples)); + } + if !self.domain().is_bounded() { + return Err(ResamplingError::UnboundedDomain); + } + + let samples: Vec = self + .domain() + .spaced_points(samples) + .unwrap() + .map(|t| self.sample_unchecked(t)) + .collect(); + 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, 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( + &self, + sample_times: impl IntoIterator, + interpolation: I, + ) -> Result, ResamplingError> + where + Self: Sized, + I: Fn(&T, &T, f32) -> T, + { + let mut times: Vec = sample_times + .into_iter() + .filter(|t| t.is_finite() && self.domain().contains(*t)) + .collect(); + times.dedup_by(|t1, t2| (*t1).eq(t2)); + if times.len() < 2 { + return Err(ResamplingError::NotEnoughSamples(times.len())); + } + times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); + let samples = times + .iter() + .copied() + .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 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. + fn resample_uneven_auto( + &self, + sample_times: impl IntoIterator, + ) -> Result, ResamplingError> + where + Self: Sized, + T: StableInterpolate, + { + let mut times: Vec = sample_times + .into_iter() + .filter(|t| t.is_finite() && self.domain().contains(*t)) + .collect(); + times.dedup_by(|t1, t2| (*t1).eq(t2)); + if times.len() < 2 { + return Err(ResamplingError::NotEnoughSamples(times.len())); + } + times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); + let samples = times + .iter() + .copied() + .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 @@ -284,6 +469,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 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 +774,196 @@ 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 { + core: EvenCore, + interpolation: I, +} + +impl Curve for SampleCurve +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 SampleCurve { + /// 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 Into>, + interpolation: I, + ) -> Result + 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 { + core: EvenCore, +} + +impl Curve for SampleAutoCurve +where + T: StableInterpolate, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.core + .sample_with(t, ::interpolate_stable) + } +} + +impl SampleAutoCurve { + /// 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 Into>) -> Result { + 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 { + core: UnevenCore, + interpolation: I, +} + +impl Curve for UnevenSampleCurve +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 UnevenSampleCurve { + /// 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 Into>, + interpolation: I, + ) -> Result { + 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 { + 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 { + core: UnevenCore, +} + +impl Curve for UnevenSampleAutoCurve +where + T: StableInterpolate, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.core + .sample_with(t, ::interpolate_stable) + } +} + +impl UnevenSampleAutoCurve { + /// 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 Into>) -> Result { + 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 { + 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(domain: Interval, value: T) -> ConstantCurve { ConstantCurve { domain, value } @@ -682,4 +1071,75 @@ 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(), f32::log2); + + // Need at least two points to sample. + let nice_try = curve.by_ref().resample_auto(1); + 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(101).unwrap(); + let step = curve.domain().length() / 100.0; + for index in 0..101 { + let test_pt = curve.domain().start() + index as f32 * step; + 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(), f32::cos); + let resampled_curve = curve.by_ref().resample_auto(1001).unwrap(); + let step = curve.domain().length() / 1000.0; + for index in 0..1001 { + let test_pt = curve.domain().start() + index as f32 * step; + 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(), f32::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(), f32::log2); + let sample_points = (0..10).map(|idx| (idx as f32).exp2()); + let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap(); + for idx in 0..10 { + let test_pt = (idx as f32).exp2(); + 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); + } } From bd91f87392e56ddd3dd0c333be000da8778157b1 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 12 Aug 2024 21:00:14 -0400 Subject: [PATCH 14/22] Fix complaint about spelling --- crates/bevy_math/src/curve/cores.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index a08f2d514bf51..024838f8e5d9f 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -1,7 +1,7 @@ //! Core data structures to be used internally in Curve implementations, encapsulating storage //! and access patterns for reuse. //! -//! The `Core` types here expose their fields publically so that it is easier to manipulate and +//! 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. From 02f13644376cd92ac1506a198ae5a44386151f3c Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 12 Aug 2024 21:26:04 -0400 Subject: [PATCH 15/22] Replace forbidden methods --- crates/bevy_math/src/curve/mod.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index a08a7011cab8a..5bd1141fffe43 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -1074,7 +1074,7 @@ mod tests { #[test] fn resampling() { - let curve = function_curve(interval(1.0, 4.0).unwrap(), f32::log2); + let curve = function_curve(interval(1.0, 4.0).unwrap(), ops::log2); // Need at least two points to sample. let nice_try = curve.by_ref().resample_auto(1); @@ -1096,7 +1096,7 @@ mod tests { } // Another example. - let curve = function_curve(interval(0.0, TAU).unwrap(), f32::cos); + let curve = function_curve(interval(0.0, TAU).unwrap(), ops::cos); let resampled_curve = curve.by_ref().resample_auto(1001).unwrap(); let step = curve.domain().length() / 1000.0; for index in 0..1001 { @@ -1112,7 +1112,7 @@ mod tests { #[test] fn uneven_resampling() { - let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), f32::exp); + 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]); @@ -1131,11 +1131,11 @@ mod tests { 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(), f32::log2); - let sample_points = (0..10).map(|idx| (idx as f32).exp2()); + 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 = (idx as f32).exp2(); + let test_pt = ops::exp2(idx as f32); let expected = curve.sample_unchecked(test_pt); assert_eq!(resampled_curve.sample_unchecked(test_pt), expected); } From 017f16b0a6d6f822b4c963fc4798e89e6125f372 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 15 Aug 2024 05:50:31 -0400 Subject: [PATCH 16/22] Update docs and logic of UnevenCore::new --- crates/bevy_math/src/curve/cores.rs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 024838f8e5d9f..19c354ac75510 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -8,6 +8,7 @@ use super::interval::Interval; use core::fmt::Debug; +use itertools::Itertools; use thiserror::Error; #[cfg(feature = "bevy_reflect")] @@ -277,29 +278,25 @@ impl UnevenCore { /// 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. - /// - /// 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 Into>) -> Result { - let timed_samples: Vec<(f32, T)> = timed_samples.into(); - + pub fn new(timed_samples: impl IntoIterator) -> Result { // Filter out non-finite sample times first so they don't interfere with sorting/deduplication. - let mut timed_samples: Vec<(f32, T)> = timed_samples + let mut timed_samples = timed_samples .into_iter() .filter(|(t, _)| t.is_finite()) - .collect(); + .collect_vec(); timed_samples - .sort_by(|(t0, _), (t1, _)| t0.partial_cmp(t1).unwrap_or(std::cmp::Ordering::Equal)); + // 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); - let (times, samples): (Vec, Vec) = timed_samples.into_iter().unzip(); - - if times.len() < 2 { + if timed_samples.len() < 2 { return Err(UnevenCoreError::NotEnoughSamples { - samples: times.len(), + samples: timed_samples.len(), }); } + + let (times, samples): (Vec, Vec) = timed_samples.into_iter().unzip(); Ok(UnevenCore { times, samples }) } From 1010d71fb5d45aaa6b1f0312c280ba1602408ebf Mon Sep 17 00:00:00 2001 From: Matty Date: Thu, 15 Aug 2024 06:41:21 -0400 Subject: [PATCH 17/22] Apply some suggestions from code review Co-authored-by: Robert Walter <26892280+RobWalt@users.noreply.github.com> --- crates/bevy_math/src/curve/cores.rs | 6 +++--- crates/bevy_math/src/curve/mod.rs | 18 +++++++----------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 19c354ac75510..8c731d2678605 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -267,7 +267,7 @@ pub struct UnevenCore { #[error("Could not construct an UnevenCore")] pub enum UnevenCoreError { /// Not enough samples were provided. - #[error("Need at least two samples to create an UnevenCore, but {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, @@ -405,7 +405,7 @@ pub enum ChunkedUnevenCoreError { /// At least two sample times are necessary to interpolate in `ChunkedUnevenCore`. #[error( - "Need at least two samples to create a ChunkedUnevenCore, but {samples} were provided" + "Need at least two unique samples to create a ChunkedUnevenCore, but {samples} were provided" )] NotEnoughSamples { /// The number of samples that were provided. @@ -513,7 +513,7 @@ impl ChunkedUnevenCore { } /// Sort the given times, deduplicate them, and filter them to only finite times. -fn filter_sort_dedup_times(times: Vec) -> Vec { +fn filter_sort_dedup_times(times: impl IntoIterator) -> Vec { // Filter before sorting/deduplication so that NAN doesn't interfere with them. let mut times: Vec = times.into_iter().filter(|t| t.is_finite()).collect(); times.sort_by(|t0, t1| t0.partial_cmp(t1).unwrap()); diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 5bd1141fffe43..34d58d6ea4646 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -475,7 +475,7 @@ pub enum ChainError { #[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 samples to construct resampled curve")] + #[error("Not enough unique samples to construct resampled curve")] NotEnoughSamples(usize), /// This resampling operation failed because of an unbounded interval. @@ -809,7 +809,7 @@ impl SampleCurve { /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. pub fn new( domain: Interval, - samples: impl Into>, + samples: impl IntoIterator, interpolation: I, ) -> Result where @@ -850,7 +850,7 @@ impl SampleAutoCurve { /// 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 Into>) -> Result { + pub fn new(domain: Interval, samples: impl IntoIterator) -> Result { Ok(Self { core: EvenCore::new(domain, samples)?, }) @@ -893,7 +893,7 @@ impl UnevenSampleCurve { /// 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 Into>, + timed_samples: impl IntoIterator<(f32, T)>, interpolation: I, ) -> Result { Ok(Self { @@ -945,7 +945,7 @@ impl UnevenSampleAutoCurve { /// 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 Into>) -> Result { + pub fn new(timed_samples: impl IntoIterator) -> Result { Ok(Self { core: UnevenCore::new(timed_samples)?, }) @@ -1084,9 +1084,7 @@ mod tests { // Because of denominators, it's not literally equal. // (This is a tradeoff against O(1) sampling.) let resampled_curve = curve.by_ref().resample_auto(101).unwrap(); - let step = curve.domain().length() / 100.0; - for index in 0..101 { - let test_pt = curve.domain().start() + index as f32 * step; + 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), @@ -1098,9 +1096,7 @@ mod tests { // Another example. let curve = function_curve(interval(0.0, TAU).unwrap(), ops::cos); let resampled_curve = curve.by_ref().resample_auto(1001).unwrap(); - let step = curve.domain().length() / 1000.0; - for index in 0..1001 { - let test_pt = curve.domain().start() + index as f32 * step; + 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), From dd44a62c705cc539f31898ccd60fe3877202a457 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 15 Aug 2024 06:16:24 -0400 Subject: [PATCH 18/22] Improve UnevenCore::map_sample_times --- crates/bevy_math/src/curve/cores.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 8c731d2678605..154da016c67ba 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -360,13 +360,17 @@ impl UnevenCore { /// 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 { - let mut timed_samples: Vec<(f32, T)> = - self.times.into_iter().map(f).zip(self.samples).collect(); - timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); - timed_samples.sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); - self.times = timed_samples.iter().map(|(t, _)| t).copied().collect(); - self.samples = timed_samples.into_iter().map(|(_, x)| x).collect(); + 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 } } @@ -515,8 +519,8 @@ impl ChunkedUnevenCore { /// Sort the given times, deduplicate them, and filter them to only finite times. fn filter_sort_dedup_times(times: impl IntoIterator) -> Vec { // Filter before sorting/deduplication so that NAN doesn't interfere with them. - let mut times: Vec = times.into_iter().filter(|t| t.is_finite()).collect(); - times.sort_by(|t0, t1| t0.partial_cmp(t1).unwrap()); + let mut times = times.into_iter().filter(|t| t.is_finite()).collect_vec(); + times.sort_by(|t0, t1| t0.total_cmp(t1)); times.dedup(); times } From a1acfff000c82198447079e31d3a731c0bb03069 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 15 Aug 2024 06:47:37 -0400 Subject: [PATCH 19/22] Make changes from code review compile --- crates/bevy_math/src/curve/cores.rs | 15 ++++++++++----- crates/bevy_math/src/curve/mod.rs | 11 +++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 154da016c67ba..a49fa70e507ee 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -141,8 +141,11 @@ impl EvenCore { /// Create a new [`EvenCore`] from the specified `domain` and `samples`. 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 Into>) -> Result { - let samples: Vec = samples.into(); + pub fn new( + domain: Interval, + samples: impl IntoIterator, + ) -> Result { + let samples: Vec = samples.into_iter().collect(); if samples.len() < 2 { return Err(EvenCoreError::NotEnoughSamples { samples: samples.len(), @@ -267,7 +270,9 @@ pub struct UnevenCore { #[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")] + #[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, @@ -517,10 +522,10 @@ impl ChunkedUnevenCore { } /// Sort the given times, deduplicate them, and filter them to only finite times. -fn filter_sort_dedup_times(times: impl IntoIterator) -> Vec { +fn filter_sort_dedup_times(times: impl IntoIterator) -> Vec { // 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(|t0, t1| t0.total_cmp(t1)); + times.sort_by(f32::total_cmp); times.dedup(); times } diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 34d58d6ea4646..6ee9b19e6f0d0 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -809,7 +809,7 @@ impl SampleCurve { /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. pub fn new( domain: Interval, - samples: impl IntoIterator, + samples: impl IntoIterator, interpolation: I, ) -> Result where @@ -850,7 +850,10 @@ impl SampleAutoCurve { /// 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) -> Result { + pub fn new( + domain: Interval, + samples: impl IntoIterator, + ) -> Result { Ok(Self { core: EvenCore::new(domain, samples)?, }) @@ -893,7 +896,7 @@ impl UnevenSampleCurve { /// 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<(f32, T)>, + timed_samples: impl IntoIterator, interpolation: I, ) -> Result { Ok(Self { @@ -945,7 +948,7 @@ impl UnevenSampleAutoCurve { /// 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) -> Result { + pub fn new(timed_samples: impl IntoIterator) -> Result { Ok(Self { core: UnevenCore::new(timed_samples)?, }) From c1d04fb711c6b1a4bbc272ebff1808236b8e81eb Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 15 Aug 2024 08:02:10 -0400 Subject: [PATCH 20/22] Update resampling methods --- crates/bevy_math/src/curve/cores.rs | 2 +- crates/bevy_math/src/curve/mod.rs | 90 +++++++++++------------------ 2 files changed, 35 insertions(+), 57 deletions(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index a49fa70e507ee..fd0be04fa02ca 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -437,7 +437,7 @@ impl ChunkedUnevenCore { /// /// Produces an error in any of the following circumstances: /// - `width` is zero. - /// - `times` has less than `2` valid entries. + /// - `times` has less than `2` valid unique entries. /// - `values` has the incorrect length relative to `times`. /// /// [type-level documentation]: ChunkedUnevenCore diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 6ee9b19e6f0d0..d787fea320f2e 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -6,6 +6,7 @@ pub mod cores; pub mod interval; pub use interval::{interval, Interval}; +use itertools::Itertools; use crate::StableInterpolate; use cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError}; @@ -243,19 +244,7 @@ pub trait Curve { Self: Sized, I: Fn(&T, &T, f32) -> T, { - if segments == 0 { - return Err(ResamplingError::NotEnoughSamples(segments)); - } - if !self.domain().is_bounded() { - return Err(ResamplingError::UnboundedDomain); - } - - let samples: Vec = self - .domain() - .spaced_points(segments + 1) - .unwrap() - .map(|t| self.sample_unchecked(t)) - .collect(); + let samples = self.samples(segments + 1)?.collect_vec(); Ok(SampleCurve { core: EvenCore { domain: self.domain(), @@ -266,26 +255,19 @@ pub trait Curve { } /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally - /// spaced values. A total of `samples` samples are used, although at least two samples are - /// required in order to produce well-formed output. If fewer than two samples are provided, - /// or if this curve has an unbounded domain, then a [`ResamplingError`] is returned. - fn resample_auto(&self, samples: usize) -> Result, ResamplingError> + /// 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, ResamplingError> where + Self: Sized, T: StableInterpolate, { - if samples < 2 { - return Err(ResamplingError::NotEnoughSamples(samples)); - } - if !self.domain().is_bounded() { - return Err(ResamplingError::UnboundedDomain); - } - - let samples: Vec = self - .domain() - .spaced_points(samples) - .unwrap() - .map(|t| self.sample_unchecked(t)) - .collect(); + let samples = self.samples(segments + 1)?.collect_vec(); Ok(SampleAutoCurve { core: EvenCore { domain: self.domain(), @@ -340,28 +322,25 @@ pub trait Curve { Self: Sized, I: Fn(&T, &T, f32) -> T, { - let mut times: Vec = sample_times + let domain = self.domain(); + let mut times = sample_times .into_iter() - .filter(|t| t.is_finite() && self.domain().contains(*t)) - .collect(); - times.dedup_by(|t1, t2| (*t1).eq(t2)); + .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())); } - times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); - let samples = times - .iter() - .copied() - .map(|t| self.sample_unchecked(t)) - .collect(); + 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 interpolation over samples - /// taken at the given set of times. The given `sample_times` are expected to contain at least + /// 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 @@ -370,6 +349,8 @@ pub trait Curve { /// /// 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, @@ -378,20 +359,17 @@ pub trait Curve { Self: Sized, T: StableInterpolate, { - let mut times: Vec = sample_times + let domain = self.domain(); + let mut times = sample_times .into_iter() - .filter(|t| t.is_finite() && self.domain().contains(*t)) - .collect(); - times.dedup_by(|t1, t2| (*t1).eq(t2)); + .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())); } - times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); - let samples = times - .iter() - .copied() - .map(|t| self.sample_unchecked(t)) - .collect(); + let samples = times.iter().map(|t| self.sample_unchecked(*t)).collect(); Ok(UnevenSampleAutoCurve { core: UnevenCore { times, samples }, }) @@ -1079,14 +1057,14 @@ mod tests { fn resampling() { let curve = function_curve(interval(1.0, 4.0).unwrap(), ops::log2); - // Need at least two points to sample. - let nice_try = curve.by_ref().resample_auto(1); + // 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(101).unwrap(); + 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!( @@ -1098,7 +1076,7 @@ mod tests { // Another example. let curve = function_curve(interval(0.0, TAU).unwrap(), ops::cos); - let resampled_curve = curve.by_ref().resample_auto(1001).unwrap(); + 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!( From 91f09b277a7faaa4739601ebc68d283751b18203 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 15 Aug 2024 08:09:09 -0400 Subject: [PATCH 21/22] Add must_use attribute to self-consuming methods --- crates/bevy_math/src/curve/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index d787fea320f2e..03cfd7c74c07f 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -54,6 +54,7 @@ pub trait Curve { /// 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(self, f: F) -> MapCurve where Self: Sized, @@ -102,6 +103,7 @@ pub trait Curve { /// let domain = my_curve.domain(); /// let eased_curve = my_curve.reparametrize(domain, |t| easing_curve.sample_unchecked(t).y); /// ``` + #[must_use] fn reparametrize(self, domain: Interval, f: F) -> ReparamCurve where Self: Sized, @@ -146,6 +148,7 @@ pub trait Curve { /// 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(self, other: C) -> CurveReparamCurve where Self: Sized, @@ -164,6 +167,7 @@ pub trait Curve { /// 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`, the output of this method /// is a `Curve<(f32, T)>`. + #[must_use] fn graph(self) -> GraphCurve where Self: Sized, @@ -397,6 +401,7 @@ pub trait Curve { } /// Flip this curve so that its tuple output is arranged the other way. + #[must_use] fn flip(self) -> impl Curve<(V, U)> where Self: Sized + Curve<(U, V)>, From 879dfd5dc42a39c86287a253ef362fbbcd3eb807 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 15 Aug 2024 14:10:20 -0400 Subject: [PATCH 22/22] Update EvenCore/UnevenCore docs --- crates/bevy_math/src/curve/cores.rs | 71 ++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index fd0be04fa02ca..abe9225c7aa44 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -65,15 +65,19 @@ impl InterpolationDatum { /// ```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(first: &T, second: &T, t: f32) -> T { /// if t >= 1.0 { /// second.clone() @@ -82,6 +86,10 @@ impl InterpolationDatum { /// } /// } /// +/// // 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 { /// core: EvenCore, /// interpolation_mode: InterpolationMode, @@ -96,6 +104,7 @@ impl InterpolationDatum { /// } /// /// 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, ::lerp), /// InterpolationMode::Step => self.core.sample_with(t, step), @@ -138,8 +147,10 @@ pub enum EvenCoreError { } impl EvenCore { - /// Create a new [`EvenCore`] from the specified `domain` and `samples`. An error is returned - /// if there are not at least 2 samples or if the given domain is unbounded. + /// 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, @@ -245,8 +256,64 @@ pub fn even_interp(domain: Interval, samples: usize, t: f32) -> InterpolationDat /// 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 { +/// core: UnevenCore, +/// interpolation_mode: InterpolationMode, +/// } +/// +/// impl Curve for RotationCurve +/// 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, ::nlerp), +/// InterpolationMode::SphericalLinear => +/// self.core.sample_with(t, ::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))]