From 708645f74570476be569f45418f762a16d74215d Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 11 Apr 2024 19:43:54 -0400 Subject: [PATCH 01/28] First draft --- crates/bevy_math/src/curve.rs | 662 ++++++++++++++++++++++++++++++++++ crates/bevy_math/src/lib.rs | 1 + 2 files changed, 663 insertions(+) create mode 100644 crates/bevy_math/src/curve.rs diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs new file mode 100644 index 0000000000000..c7d5e4b53852b --- /dev/null +++ b/crates/bevy_math/src/curve.rs @@ -0,0 +1,662 @@ +//! Houses the [`Curve`] trait together with the [`Interpolable`] trait that it depends on. + +use std::{cmp::max, marker::PhantomData}; +use crate::Quat; +// use serde::{de::DeserializeOwned, Serialize}; + +use crate::VectorSpace; + +/// A trait for types whose values can be intermediately interpolated between two given values +/// with an auxiliary parameter. +pub trait Interpolable: Clone { + /// Interpolate between this value and the `other` given value using the parameter `t`. + /// Note that the parameter `t` is not necessarily clamped to lie between `0` and `1`. + fn interpolate(&self, other: &Self, t: f32) -> Self; +} + +impl Interpolable for (S, T) +where + S: Interpolable, + T: Interpolable, +{ + fn interpolate(&self, other: &Self, t: f32) -> Self { + ( + self.0.interpolate(&other.0, t), + self.1.interpolate(&other.1, t), + ) + } +} + +impl Interpolable for T +where + T: VectorSpace, +{ + fn interpolate(&self, other: &Self, t: f32) -> Self { + self.lerp(*other, t) + } +} + +impl Interpolable for Quat { + fn interpolate(&self, other: &Self, t: f32) -> Self { + self.slerp(*other, t) + } +} + + +/// 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 +where + T: Interpolable, +{ + /// The point at which parameter values of this curve end. That is, this curve is parametrized + /// on the interval `[0, self.duration()]`. + fn duration(&self) -> f32; + + /// Sample a point on this curve at the parameter value `t`, extracting the associated value. + fn sample(&self, t: f32) -> T; + + /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally + /// spaced values. A total of `samples` samples are used. + /// + /// Panics if `samples == 0`. + fn resample(&self, samples: usize) -> SampleCurve { + assert!(samples != 0); + + // When `samples` is 1, we just record the starting point, and `step` doesn't matter. + let subdivisions = max(1, samples - 1); + let step = self.duration() / subdivisions as f32; + let samples: Vec = (0..samples).map(|s| self.sample(s as f32 * step)).collect(); + SampleCurve { + duration: self.duration(), + samples, + } + } + + /// 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 be strictly + /// increasing and nonempty. + fn resample_uneven(&self, sample_times: impl IntoIterator) -> UnevenSampleCurve { + let mut iter = sample_times.into_iter(); + let Some(first) = iter.next() else { + panic!("Empty iterator supplied") + }; + // Offset by the first element so that we get a curve starting at zero. + let first_sample = self.sample(first); + let mut timed_samples = vec![(0.0, first_sample)]; + timed_samples.extend(iter.map(|t| (t - first, self.sample(t)))); + UnevenSampleCurve { timed_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: impl Fn(T) -> S) -> impl Curve + where + Self: Sized, + S: Interpolable, + { + 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 `duration` will be the duration of the new curve. The + /// function `f` is expected to take `[0, duration]` into `[0, self.duration]`. + /// + /// 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(1.0, 1.0); + /// let dur = my_curve.duration(); + /// let scaled_curve = my_curve.reparametrize(dur * 2.0, |t| t / 2.0); + /// ``` + /// This kind of linear remapping is provided by the convenience method + /// [`Curve::reparametrize_linear`], which requires only the desired duration for the new curve. + /// + /// # Examples + /// ``` + /// // Reverse a curve: + /// # use bevy_math::curve::*; + /// # use bevy_math::vec2; + /// # let my_curve = constant_curve(1.0, 1.0); + /// let dur = my_curve.duration(); + /// let reversed_curve = my_curve.reparametrize(dur, |t| dur - t); + /// + /// // Take a segment of a curve: + /// # let my_curve = constant_curve(1.0, 1.0); + /// let curve_segment = my_curve.reparametrize(0.5, |t| 0.5 + t); + /// + /// // Reparametrize by an easing curve: + /// # let my_curve = constant_curve(1.0, 1.0); + /// # let easing_curve = constant_curve(1.0, vec2(1.0, 1.0)); + /// let dur = my_curve.duration(); + /// let eased_curve = my_curve.reparametrize(dur, |t| easing_curve.sample(t).y); + /// ``` + /// + /// # Panics + /// Panics if `duration` is not greater than `0.0`. + fn reparametrize(self, duration: f32, f: impl Fn(f32) -> f32) -> impl Curve + where + Self: Sized, + { + assert!(duration > 0.0); + ReparamCurve { + duration, + base: self, + f, + _phantom: PhantomData, + } + } + + /// Linearly reparametrize this [`Curve`], producing a new curve whose duration is the given + /// `duration` instead of the current one. + fn reparametrize_linear(self, duration: f32) -> impl Curve + where + Self: Sized, + { + assert!(duration > 0.0); + let old_duration = self.duration(); + Curve::reparametrize(self, duration, move |t| t * (old_duration / duration)) + } + + /// Reparametrize this [`Curve`] by sampling from another curve. + fn reparametrize_by_curve(self, other: &impl Curve) -> impl Curve + where + Self: Sized, + { + self.reparametrize(other.duration(), |t| other.sample(t)) + } + + /// 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) -> impl Curve<(f32, T)> + where + Self: Sized, + { + GraphCurve { + base: self, + _phantom: PhantomData, + } + } + + /// Create a new [`Curve`] by joining 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 duration of the new curve is the smaller of the two + /// between `self` and `other`. + fn and(self, other: C) -> impl Curve<(T, S)> + where + Self: Sized, + S: Interpolable, + C: Curve + Sized, + { + ProductCurve { + first: self, + second: other, + _phantom: PhantomData, + } + } +} + +/// A [`Curve`] which takes a constant value over its duration. +pub struct ConstantCurve +where + T: Interpolable, +{ + duration: f32, + value: T, +} + +impl Curve for ConstantCurve +where + T: Interpolable, +{ + #[inline] + fn duration(&self) -> f32 { + self.duration + } + + #[inline] + fn sample(&self, _t: f32) -> T { + self.value.clone() + } +} + +/// A [`Curve`] defined by a function. +pub struct FunctionCurve +where + T: Interpolable, + F: Fn(f32) -> T, +{ + duration: f32, + f: F, +} + +impl Curve for FunctionCurve +where + T: Interpolable, + F: Fn(f32) -> T, +{ + #[inline] + fn duration(&self) -> f32 { + self.duration + } + + #[inline] + fn sample(&self, t: f32) -> T { + (self.f)(t) + } +} + +/// A [`Curve`] that is defined by neighbor interpolation over a set of samples. +pub struct SampleCurve +where + T: Interpolable, +{ + duration: f32, + + /// The list of samples that define this curve by interpolation. + pub samples: Vec, +} + +impl SampleCurve +where + T: Interpolable, +{ + /// Like [`Curve::map`], but with a concrete return type. + pub fn map_concrete(self, f: impl Fn(T) -> S) -> SampleCurve + where + S: Interpolable, + { + let new_samples: Vec = self.samples.into_iter().map(f).collect(); + SampleCurve { + duration: self.duration, + samples: new_samples, + } + } + + /// Like [`Curve::graph`], but with a concrete return type. + pub fn graph_concrete(self) -> SampleCurve<(f32, T)> { + let subdivisions = max(1, self.samples.len() - 1); + let step = self.duration() / subdivisions as f32; + let times: Vec = (0..self.samples.len()).map(|s| s as f32 * step).collect(); + let new_samples: Vec<(f32, T)> = times.into_iter().zip(self.samples).collect(); + SampleCurve { + duration: self.duration, + samples: new_samples, + } + } +} + +impl Curve for SampleCurve +where + T: Interpolable, +{ + #[inline] + fn duration(&self) -> f32 { + self.duration + } + + #[inline] + fn sample(&self, t: f32) -> T { + let num_samples = self.samples.len(); + // If there is only one sample, then we return the single sample point. We also clamp `t` + // to `[0, self.duration]` here. + if num_samples == 1 || t <= 0.0 { + return self.samples[0].clone(); + } + if t >= self.duration { + return self.samples[self.samples.len() - 1].clone(); + } + + // Inside the curve itself, interpolate between the two nearest sample values. + let subdivs = num_samples - 1; + let step = self.duration / subdivs as f32; + let lower_index = (t / step).floor() as usize; + let upper_index = (t / step).ceil() as usize; + let f = (t / step).fract(); + self.samples[lower_index].interpolate(&self.samples[upper_index], f) + } + + fn map(self, f: impl Fn(T) -> S) -> impl Curve + where + Self: Sized, + S: Interpolable, + { + self.map_concrete(f) + } + + fn graph(self) -> impl Curve<(f32, T)> + where + Self: Sized, + { + self.graph_concrete() + } +} + +/// A [`Curve`] that is defined by interpolation over unevenly spaced samples. +pub struct UnevenSampleCurve +where + T: Interpolable, +{ + timed_samples: Vec<(f32, T)>, +} + +impl UnevenSampleCurve +where + T: Interpolable, +{ + /// Like [`Curve::map`], but with a concrete return type.. + pub fn map_concrete(self, f: impl Fn(T) -> S) -> UnevenSampleCurve + where + S: Interpolable, + { + let new_samples: Vec<(f32, S)> = self + .timed_samples + .into_iter() + .map(|(t, x)| (t, f(x))) + .collect(); + UnevenSampleCurve { + timed_samples: new_samples, + } + } + + /// Like [`Curve::graph`], but with a concrete return type. + pub fn graph_concrete(self) -> UnevenSampleCurve<(f32, T)> { + let new_samples: Vec<(f32, (f32, T))> = self + .timed_samples + .into_iter() + .map(|(t, x)| (t, (t, x))) + .collect(); + UnevenSampleCurve { + timed_samples: new_samples, + } + } +} + +impl Curve for UnevenSampleCurve +where + T: Interpolable, +{ + #[inline] + fn duration(&self) -> f32 { + self.timed_samples.last().unwrap().0 + } + + #[inline] + fn sample(&self, t: f32) -> T { + match self + .timed_samples + .binary_search_by(|(pt, _)| pt.partial_cmp(&t).unwrap()) + { + Ok(index) => self.timed_samples[index].1.clone(), + Err(index) => { + if index == 0 { + self.timed_samples.first().unwrap().1.clone() + } else if index == self.timed_samples.len() { + self.timed_samples.last().unwrap().1.clone() + } else { + let (t_lower, v_lower) = self.timed_samples.get(index - 1).unwrap(); + let (t_upper, v_upper) = self.timed_samples.get(index).unwrap(); + let s = (t - t_lower) / (t_upper - t_lower); + v_lower.interpolate(v_upper, s) + } + } + } + } + + fn map(self, f: impl Fn(T) -> S) -> impl Curve + where + Self: Sized, + S: Interpolable, + { + self.map_concrete(f) + } + + fn graph(self) -> impl Curve<(f32, T)> + where + Self: Sized, + { + self.graph_concrete() + } +} + +/// A [`Curve`] whose samples are defined by mapping samples from another curve through a +/// given function. +pub struct MapCurve +where + S: Interpolable, + T: Interpolable, + C: Curve, + F: Fn(S) -> T, +{ + preimage: C, + f: F, + _phantom: PhantomData<(S, T)>, +} + +impl Curve for MapCurve +where + S: Interpolable, + T: Interpolable, + C: Curve, + F: Fn(S) -> T, +{ + #[inline] + fn duration(&self) -> f32 { + self.preimage.duration() + } + + #[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. +pub struct ReparamCurve +where + T: Interpolable, + C: Curve, + F: Fn(f32) -> f32, +{ + duration: f32, + base: C, + f: F, + _phantom: PhantomData, +} + +impl Curve for ReparamCurve +where + T: Interpolable, + C: Curve, + F: Fn(f32) -> f32, +{ + #[inline] + fn duration(&self) -> f32 { + self.duration + } + + #[inline] + fn sample(&self, t: f32) -> T { + self.base.sample((self.f)(t)) + } +} + +/// A [`Curve`] that is the graph of another curve over its parameter space. +pub struct GraphCurve +where + T: Interpolable, + C: Curve, +{ + base: C, + _phantom: PhantomData, +} + +impl Curve<(f32, T)> for GraphCurve +where + T: Interpolable, + C: Curve, +{ + #[inline] + fn duration(&self) -> f32 { + self.base.duration() + } + + #[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. +pub struct ProductCurve +where + S: Interpolable, + T: Interpolable, + C: Curve, + D: Curve, +{ + first: C, + second: D, + _phantom: PhantomData<(S, T)>, +} + +impl Curve<(S, T)> for ProductCurve +where + S: Interpolable, + T: Interpolable, + C: Curve, + D: Curve, +{ + #[inline] + fn duration(&self) -> f32 { + f32::min(self.first.duration(), self.second.duration()) + } + + #[inline] + fn sample(&self, t: f32) -> (S, T) { + (self.first.sample(t), self.second.sample(t)) + } +} + +// Experimental stuff: + +// TODO: See how much this needs to be extended / whether it's actually useful. +// The actual point here is to give access to additional trait constraints that are +// satisfied by the output, but not guaranteed depending on the actual data +// that underpins the invoking implementation. + +// pub trait MapConcreteCurve: Curve + Serialize + DeserializeOwned +// where T: Interpolable { +// fn map_concrete(self, f: impl Fn(T) -> S) -> impl MapConcreteCurve +// where S: Interpolable; +// } + +// Library functions: + +/// Create a [`Curve`] that constantly takes the given `value` over the given `duration`. +pub fn constant_curve(duration: f32, value: T) -> impl Curve { + ConstantCurve { duration, value } +} + +/// Convert the given function `f` into a [`Curve`] with the given `duration`, sampled by +/// evaluating the function. +pub fn function_curve(duration: f32, f: F) -> impl Curve +where + T: Interpolable, + F: Fn(f32) -> T, +{ + FunctionCurve { duration, f } +} + +/// Flip a curve that outputs tuples so that the tuples are arranged the other way. +pub fn flip(curve: impl Curve<(S, T)>) -> impl Curve<(T, S)> +where + S: Interpolable, + T: Interpolable, +{ + curve.map(|(s, t)| (t, s)) +} + +/// An error indicating that the implicit function theorem algorithm failed to apply because +/// the input curve did not meet its criteria. +pub struct IftError; + +/// Given a monotone `curve`, produces the curve that it is the graph of, up to reparametrization. +/// This is an algorithmic manifestation of the implicit function theorem; it is a numerical +/// procedure which is only performed to the specified resolutions. +/// +/// The `search_resolution` dictates how many samples are taken of the input curve; linear +/// interpolation is used between these samples to estimate the inverse image. +/// +/// The `outgoing_resolution` dictates the number of samples that are used in the construction of +/// the output itself. +/// +/// The input curve must have its first x-value be `0` or an error will be returned. Furthermore, +/// if the curve is non-monotone, the output of this function may be nonsensical even if an error +/// does not occur. +pub fn ift( + curve: &impl Curve<(f32, T)>, + search_resolution: usize, + outgoing_resolution: usize, +) -> Result, IftError> +where + T: Interpolable, +{ + // The duration of the output curve is the maximum x-value of the input curve. + let (duration, _) = curve.sample(curve.duration()); + let discrete_curve = curve.resample(search_resolution); + + let subdivisions = max(1, outgoing_resolution - 1); + let step = duration / subdivisions as f32; + let times: Vec = (0..outgoing_resolution).map(|s| s as f32 * step).collect(); + + let mut values: Vec = vec![]; + for t in times { + // Find a value on the curve where the x-value is close to `t`. + match discrete_curve + .samples + .binary_search_by(|(x, _y)| x.partial_cmp(&t).unwrap()) + { + // We found an exact match in our samples (pretty unlikely). + Ok(index) => { + let y = discrete_curve.samples[index].1.clone(); + values.push(y); + } + + // We did not find an exact match, so we must interpolate. + Err(index) => { + // The value should be between `index - 1` and `index`. + // If `index` is the sample length or 0, then something went wrong; `t` is outside + // of the range of the function projection. + if index == 0 || index == search_resolution { + return Err(IftError); + } else { + let (t_lower, y_lower) = discrete_curve.samples.get(index - 1).unwrap(); + let (t_upper, y_upper) = discrete_curve.samples.get(index).unwrap(); + if t_lower >= t_upper { + return Err(IftError); + } + // Inverse lerp on projected values to interpolate the y-value. + let s = (t - t_lower) / (t_upper - t_lower); + let value = y_lower.interpolate(y_upper, s); + values.push(value); + } + } + } + } + Ok(SampleCurve { + duration, + samples: values, + }) +} diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index c98c328d1befa..7fb50a4510558 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -16,6 +16,7 @@ mod aspect_ratio; pub mod bounding; mod common_traits; pub mod cubic_splines; +pub mod curve; mod direction; mod float_ord; pub mod primitives; From 71ab763c294dee3e97bffbc6e02694753b2d7ee9 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 13 Apr 2024 14:59:27 -0400 Subject: [PATCH 02/28] Added Interval and refactored to use it as the curve domain --- crates/bevy_math/src/curve.rs | 593 ++++++++++++++++++++++------------ 1 file changed, 394 insertions(+), 199 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index c7d5e4b53852b..06af87aaeb8ec 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -1,10 +1,100 @@ -//! Houses the [`Curve`] trait together with the [`Interpolable`] trait that it depends on. +//! Houses the [`Curve`] trait together with the [`Interpolable`] trait and the [`Interval`] +//! struct that it depends on. + +use crate::{Quat, VectorSpace}; +use std::{ + cmp::{max, max_by, min_by}, + marker::PhantomData, + ops::RangeInclusive, +}; + +/// A nonempty closed interval, possibly infinite in either direction. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct Interval { + start: f32, + end: f32, +} + +/// An error that indicates that an operation would have returned an invalid [`Interval`]. +#[derive(Debug)] +pub struct InvalidIntervalError; + +/// An error indicating that an infinite interval was used where it was inappropriate. +#[derive(Debug)] +pub struct InfiniteIntervalError; + +impl Interval { + /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be infinite + /// but cannot be empty; invalid parameters will result in an error. + 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 this interval is finite. + #[inline] + pub fn is_finite(self) -> bool { + self.length().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) + } + + /// Clamp the given `value` to lie within this interval. + #[inline] + pub fn clamp(self, value: f32) -> f32 { + value.clamp(self.start, self.end) + } -use std::{cmp::max, marker::PhantomData}; -use crate::Quat; -// use serde::{de::DeserializeOwned, Serialize}; + /// Get the linear map which maps this curve onto the `other` one. Returns an error if either + /// interval is infinite. + pub 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) + } +} -use crate::VectorSpace; +impl TryFrom> for Interval { + type Error = InvalidIntervalError; + fn try_from(range: RangeInclusive) -> Result { + Interval::new(*range.start(), *range.end()) + } +} /// A trait for types whose values can be intermediately interpolated between two given values /// with an auxiliary parameter. @@ -42,6 +132,14 @@ impl Interpolable for Quat { } } +/// An error indicating that a resampling operation could not be performed because of +/// malformed inputs. +pub enum ResamplingError { + /// This resampling operation was not provided with enough samples to have well-formed output. + NotEnoughSamples(usize), + /// This resampling operation failed because of an unbounded interval. + InfiniteInterval(InfiniteIntervalError), +} /// 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 @@ -50,43 +148,75 @@ pub trait Curve where T: Interpolable, { - /// The point at which parameter values of this curve end. That is, this curve is parametrized - /// on the interval `[0, self.duration()]`. - fn duration(&self) -> f32; + /// 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 values. A total of `samples` samples are used. - /// - /// Panics if `samples == 0`. - fn resample(&self, samples: usize) -> SampleCurve { - assert!(samples != 0); + /// 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(&self, samples: usize) -> Result, ResamplingError> { + if samples < 2 { + return Err(ResamplingError::NotEnoughSamples(samples)); + } + if !self.domain().is_finite() { + return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); + } // When `samples` is 1, we just record the starting point, and `step` doesn't matter. let subdivisions = max(1, samples - 1); - let step = self.duration() / subdivisions as f32; + let step = self.domain().length() / subdivisions as f32; let samples: Vec = (0..samples).map(|s| self.sample(s as f32 * step)).collect(); - SampleCurve { - duration: self.duration(), + Ok(SampleCurve { + domain: self.domain(), samples, - } + }) } /// 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 be strictly - /// increasing and nonempty. - fn resample_uneven(&self, sample_times: impl IntoIterator) -> UnevenSampleCurve { - let mut iter = sample_times.into_iter(); - let Some(first) = iter.next() else { - panic!("Empty iterator supplied") - }; - // Offset by the first element so that we get a curve starting at zero. - let first_sample = self.sample(first); - let mut timed_samples = vec![(0.0, first_sample)]; - timed_samples.extend(iter.map(|t| (t - first, self.sample(t)))); - UnevenSampleCurve { timed_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 range. + /// + /// Irredundant 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 [`UnevenSampleCurve`] stretches between the first and last + /// sample times of the iterator. + fn resample_uneven( + &self, + sample_times: impl IntoIterator, + ) -> Result, ResamplingError> { + 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 timed_samples = times.into_iter().map(|t| (t, self.sample(t))).collect(); + Ok(UnevenSampleCurve { timed_samples }) } /// Create a new curve by mapping the values of this curve via a function `f`; i.e., if the @@ -106,8 +236,8 @@ where /// 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 `duration` will be the duration of the new curve. The - /// function `f` is expected to take `[0, duration]` into `[0, self.duration]`. + /// 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 @@ -115,57 +245,54 @@ where /// factor rather than multiplying: /// ``` /// # use bevy_math::curve::*; - /// # let my_curve = constant_curve(1.0, 1.0); - /// let dur = my_curve.duration(); - /// let scaled_curve = my_curve.reparametrize(dur * 2.0, |t| t / 2.0); + /// 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 duration for the new curve. + /// [`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(1.0, 1.0); - /// let dur = my_curve.duration(); - /// let reversed_curve = my_curve.reparametrize(dur, |t| dur - t); + /// 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(1.0, 1.0); - /// let curve_segment = my_curve.reparametrize(0.5, |t| 0.5 + t); + /// # 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(1.0, 1.0); - /// # let easing_curve = constant_curve(1.0, vec2(1.0, 1.0)); - /// let dur = my_curve.duration(); - /// let eased_curve = my_curve.reparametrize(dur, |t| easing_curve.sample(t).y); + /// # 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); /// ``` - /// - /// # Panics - /// Panics if `duration` is not greater than `0.0`. - fn reparametrize(self, duration: f32, f: impl Fn(f32) -> f32) -> impl Curve + fn reparametrize(self, domain: Interval, f: impl Fn(f32) -> f32) -> impl Curve where Self: Sized, { - assert!(duration > 0.0); ReparamCurve { - duration, + domain, base: self, f, _phantom: PhantomData, } } - /// Linearly reparametrize this [`Curve`], producing a new curve whose duration is the given - /// `duration` instead of the current one. - fn reparametrize_linear(self, duration: f32) -> impl 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. + fn reparametrize_linear(self, domain: Interval) -> Result, InfiniteIntervalError> where Self: Sized, { - assert!(duration > 0.0); - let old_duration = self.duration(); - Curve::reparametrize(self, duration, move |t| t * (old_duration / duration)) + let f = domain.linear_map_to(self.domain())?; + Ok(self.reparametrize(domain, f)) } /// Reparametrize this [`Curve`] by sampling from another curve. @@ -173,7 +300,7 @@ where where Self: Sized, { - self.reparametrize(other.duration(), |t| other.sample(t)) + self.reparametrize(other.domain(), |t| other.sample(t)) } /// Create a new [`Curve`] which is the graph of this one; that is, its output includes the @@ -191,28 +318,31 @@ where /// Create a new [`Curve`] by joining 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 duration of the new curve is the smaller of the two - /// between `self` and `other`. - fn and(self, other: C) -> impl Curve<(T, S)> + /// 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, S: Interpolable, C: Curve + Sized, { - ProductCurve { + let domain = self.domain().intersect(other.domain())?; + Ok(ProductCurve { + domain, first: self, second: other, _phantom: PhantomData, - } + }) } } -/// A [`Curve`] which takes a constant value over its duration. +/// A [`Curve`] which takes a constant value over its domain. pub struct ConstantCurve where T: Interpolable, { - duration: f32, + domain: Interval, value: T, } @@ -221,8 +351,8 @@ where T: Interpolable, { #[inline] - fn duration(&self) -> f32 { - self.duration + fn domain(&self) -> Interval { + self.domain } #[inline] @@ -232,23 +362,23 @@ where } /// A [`Curve`] defined by a function. -pub struct FunctionCurve +pub struct FunctionCurve where T: Interpolable, F: Fn(f32) -> T, { - duration: f32, + domain: Interval, f: F, } -impl Curve for FunctionCurve +impl Curve for FunctionCurve where T: Interpolable, F: Fn(f32) -> T, { #[inline] - fn duration(&self) -> f32 { - self.duration + fn domain(&self) -> Interval { + self.domain } #[inline] @@ -262,10 +392,11 @@ pub struct SampleCurve where T: Interpolable, { - duration: f32, - - /// The list of samples that define this curve by interpolation. - pub samples: Vec, + domain: Interval, + /// The samples that make up this [`SampleCurve`] by interpolation. + /// + /// Invariant: this must always have a length of at least 2. + samples: Vec, } impl SampleCurve @@ -279,19 +410,21 @@ where { let new_samples: Vec = self.samples.into_iter().map(f).collect(); SampleCurve { - duration: self.duration, + domain: self.domain, samples: new_samples, } } /// Like [`Curve::graph`], but with a concrete return type. pub fn graph_concrete(self) -> SampleCurve<(f32, T)> { - let subdivisions = max(1, self.samples.len() - 1); - let step = self.duration() / subdivisions as f32; - let times: Vec = (0..self.samples.len()).map(|s| s as f32 * step).collect(); + let subdivisions = self.samples.len() - 1; + let step = self.domain.length() / subdivisions as f32; + let times: Vec = (0..self.samples.len()) + .map(|s| self.domain.start() + (s as f32 * step)) + .collect(); let new_samples: Vec<(f32, T)> = times.into_iter().zip(self.samples).collect(); SampleCurve { - duration: self.duration, + domain: self.domain, samples: new_samples, } } @@ -302,28 +435,22 @@ where T: Interpolable, { #[inline] - fn duration(&self) -> f32 { - self.duration + fn domain(&self) -> Interval { + self.domain } #[inline] fn sample(&self, t: f32) -> T { - let num_samples = self.samples.len(); - // If there is only one sample, then we return the single sample point. We also clamp `t` - // to `[0, self.duration]` here. - if num_samples == 1 || t <= 0.0 { - return self.samples[0].clone(); - } - if t >= self.duration { - return self.samples[self.samples.len() - 1].clone(); - } + // We clamp `t` to the domain. + let t = self.domain.clamp(t); // Inside the curve itself, interpolate between the two nearest sample values. - let subdivs = num_samples - 1; - let step = self.duration / subdivs as f32; - let lower_index = (t / step).floor() as usize; - let upper_index = (t / step).ceil() as usize; - let f = (t / step).fract(); + let subdivs = self.samples.len() - 1; + let step = self.domain.length() / subdivs as f32; + let t_shifted = t - self.domain.start(); + let lower_index = (t_shifted / step).floor() as usize; + let upper_index = (t_shifted / step).ceil() as usize; + let f = (t_shifted / step).fract(); self.samples[lower_index].interpolate(&self.samples[upper_index], f) } @@ -348,6 +475,10 @@ pub struct UnevenSampleCurve where T: Interpolable, { + /// The timed that make up this [`UnevenSampleCurve`] by interpolation. + /// + /// Invariants: this must always have a length of at least 2, be sorted by time, and have no + /// duplicated or non-finite times. timed_samples: Vec<(f32, T)>, } @@ -381,6 +512,20 @@ where timed_samples: new_samples, } } + + /// This [`UnevenSampleCurve`], 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 resorted 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { + self.timed_samples.iter_mut().for_each(|(t, _)| *t = f(*t)); + self.timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); + self.timed_samples + .sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); + self + } } impl Curve for UnevenSampleCurve @@ -388,8 +533,10 @@ where T: Interpolable, { #[inline] - fn duration(&self) -> f32 { - self.timed_samples.last().unwrap().0 + fn domain(&self) -> Interval { + let start = self.timed_samples.first().unwrap().0; + let end = self.timed_samples.last().unwrap().0; + Interval::new(start, end).unwrap() } #[inline] @@ -452,14 +599,41 @@ where F: Fn(S) -> T, { #[inline] - fn duration(&self) -> f32 { - self.preimage.duration() + fn domain(&self) -> Interval { + self.preimage.domain() } #[inline] fn sample(&self, t: f32) -> T { (self.f)(self.preimage.sample(t)) } + + // Specialized implementation of [`Curve::map`] that reuses data. + fn map(self, g: impl Fn(T) -> R) -> impl Curve + where + Self: Sized, + R: Interpolable, + { + let gf = move |x| g((self.f)(x)); + MapCurve { + preimage: self.preimage, + f: gf, + _phantom: PhantomData, + } + } + + fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve + where + Self: Sized, + { + MapReparamCurve { + reparam_domain: domain, + base: self.preimage, + forward_map: self.f, + reparam_map: g, + _phantom: PhantomData, + } + } } /// A [`Curve`] whose sample space is mapped onto that of some base curve's before sampling. @@ -469,7 +643,7 @@ where C: Curve, F: Fn(f32) -> f32, { - duration: f32, + domain: Interval, base: C, f: F, _phantom: PhantomData, @@ -482,14 +656,110 @@ where F: Fn(f32) -> f32, { #[inline] - fn duration(&self) -> f32 { - self.duration + fn domain(&self) -> Interval { + self.domain } #[inline] fn sample(&self, t: f32) -> T { self.base.sample((self.f)(t)) } + + // Specialized implementation of [`Curve::reparametrize`] that reuses data. + fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve + where + Self: Sized, + { + let fg = move |t| (self.f)(g(t)); + ReparamCurve { + domain, + base: self.base, + f: fg, + _phantom: PhantomData, + } + } + + fn map(self, g: impl Fn(T) -> S) -> impl Curve + where + Self: Sized, + S: Interpolable, + { + MapReparamCurve { + reparam_domain: self.domain, + base: self.base, + forward_map: g, + reparam_map: self.f, + _phantom: PhantomData, + } + } +} + +/// A [`Curve`] structure that holds both forward and backward remapping information +/// in order to optimize repeated calls of [`Curve::map`] and [`Curve::reparametrize`]. +/// +/// Briefly, the point is that the curve just absorbs new functions instead of rebasing +/// itself inside new structs. +pub struct MapReparamCurve +where + S: Interpolable, + T: Interpolable, + C: Curve, + F: Fn(S) -> T, + G: Fn(f32) -> f32, +{ + reparam_domain: Interval, + base: C, + forward_map: F, + reparam_map: G, + _phantom: PhantomData<(S, T)>, +} + +impl Curve for MapReparamCurve +where + S: Interpolable, + T: Interpolable, + C: Curve, + F: Fn(S) -> T, + G: Fn(f32) -> f32, +{ + #[inline] + fn domain(&self) -> Interval { + self.reparam_domain + } + + #[inline] + fn sample(&self, t: f32) -> T { + (self.forward_map)(self.base.sample((self.reparam_map)(t))) + } + + fn map(self, g: impl Fn(T) -> R) -> impl Curve + where + Self: Sized, + R: Interpolable, + { + let gf = move |x| g((self.forward_map)(x)); + MapReparamCurve { + reparam_domain: self.reparam_domain, + base: self.base, + forward_map: gf, + reparam_map: self.reparam_map, + _phantom: PhantomData, + } + } + + fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve + where + Self: Sized, + { + let fg = move |t| (self.reparam_map)(g(t)); + MapReparamCurve { + reparam_domain: domain, + base: self.base, + forward_map: self.forward_map, + reparam_map: fg, + _phantom: PhantomData, + } + } } /// A [`Curve`] that is the graph of another curve over its parameter space. @@ -508,8 +778,8 @@ where C: Curve, { #[inline] - fn duration(&self) -> f32 { - self.base.duration() + fn domain(&self) -> Interval { + self.base.domain() } #[inline] @@ -526,6 +796,7 @@ where C: Curve, D: Curve, { + domain: Interval, first: C, second: D, _phantom: PhantomData<(S, T)>, @@ -539,8 +810,8 @@ where D: Curve, { #[inline] - fn duration(&self) -> f32 { - f32::min(self.first.duration(), self.second.duration()) + fn domain(&self) -> Interval { + self.domain } #[inline] @@ -549,34 +820,31 @@ where } } -// Experimental stuff: - -// TODO: See how much this needs to be extended / whether it's actually useful. -// The actual point here is to give access to additional trait constraints that are -// satisfied by the output, but not guaranteed depending on the actual data -// that underpins the invoking implementation. +// Library functions: -// pub trait MapConcreteCurve: Curve + Serialize + DeserializeOwned -// where T: Interpolable { -// fn map_concrete(self, f: impl Fn(T) -> S) -> impl MapConcreteCurve -// where S: Interpolable; -// } +/// Create an [`Interval`] with a given `start` and `end`. Alias of [`Interval::new`]. +pub fn interval(start: f32, end: f32) -> Result { + Interval::new(start, end) +} -// Library functions: +/// The [`Interval`] from negative infinity to infinity. +pub fn everywhere() -> Interval { + Interval::new(f32::NEG_INFINITY, f32::INFINITY).unwrap() +} -/// Create a [`Curve`] that constantly takes the given `value` over the given `duration`. -pub fn constant_curve(duration: f32, value: T) -> impl Curve { - ConstantCurve { duration, value } +/// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. +pub fn constant_curve(domain: Interval, value: T) -> impl Curve { + ConstantCurve { domain, value } } -/// Convert the given function `f` into a [`Curve`] with the given `duration`, sampled by +/// Convert the given function `f` into a [`Curve`] with the given `domain`, sampled by /// evaluating the function. -pub fn function_curve(duration: f32, f: F) -> impl Curve -where +pub fn function_curve(domain: Interval, f: F) -> impl Curve +where T: Interpolable, F: Fn(f32) -> T, { - FunctionCurve { duration, f } + FunctionCurve { domain, f } } /// Flip a curve that outputs tuples so that the tuples are arranged the other way. @@ -587,76 +855,3 @@ where { curve.map(|(s, t)| (t, s)) } - -/// An error indicating that the implicit function theorem algorithm failed to apply because -/// the input curve did not meet its criteria. -pub struct IftError; - -/// Given a monotone `curve`, produces the curve that it is the graph of, up to reparametrization. -/// This is an algorithmic manifestation of the implicit function theorem; it is a numerical -/// procedure which is only performed to the specified resolutions. -/// -/// The `search_resolution` dictates how many samples are taken of the input curve; linear -/// interpolation is used between these samples to estimate the inverse image. -/// -/// The `outgoing_resolution` dictates the number of samples that are used in the construction of -/// the output itself. -/// -/// The input curve must have its first x-value be `0` or an error will be returned. Furthermore, -/// if the curve is non-monotone, the output of this function may be nonsensical even if an error -/// does not occur. -pub fn ift( - curve: &impl Curve<(f32, T)>, - search_resolution: usize, - outgoing_resolution: usize, -) -> Result, IftError> -where - T: Interpolable, -{ - // The duration of the output curve is the maximum x-value of the input curve. - let (duration, _) = curve.sample(curve.duration()); - let discrete_curve = curve.resample(search_resolution); - - let subdivisions = max(1, outgoing_resolution - 1); - let step = duration / subdivisions as f32; - let times: Vec = (0..outgoing_resolution).map(|s| s as f32 * step).collect(); - - let mut values: Vec = vec![]; - for t in times { - // Find a value on the curve where the x-value is close to `t`. - match discrete_curve - .samples - .binary_search_by(|(x, _y)| x.partial_cmp(&t).unwrap()) - { - // We found an exact match in our samples (pretty unlikely). - Ok(index) => { - let y = discrete_curve.samples[index].1.clone(); - values.push(y); - } - - // We did not find an exact match, so we must interpolate. - Err(index) => { - // The value should be between `index - 1` and `index`. - // If `index` is the sample length or 0, then something went wrong; `t` is outside - // of the range of the function projection. - if index == 0 || index == search_resolution { - return Err(IftError); - } else { - let (t_lower, y_lower) = discrete_curve.samples.get(index - 1).unwrap(); - let (t_upper, y_upper) = discrete_curve.samples.get(index).unwrap(); - if t_lower >= t_upper { - return Err(IftError); - } - // Inverse lerp on projected values to interpolate the y-value. - let s = (t - t_lower) / (t_upper - t_lower); - let value = y_lower.interpolate(y_upper, s); - values.push(value); - } - } - } - } - Ok(SampleCurve { - duration, - samples: values, - }) -} From df4629c98f45c3657708735a6c4a11c9e588b30e Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 18 Apr 2024 09:19:52 -0400 Subject: [PATCH 03/28] Ensured object safety, added Deref blanket impl supported by method by_ref --- crates/bevy_math/src/curve.rs | 56 +++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index 06af87aaeb8ec..016a79232b5f2 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -5,7 +5,7 @@ use crate::{Quat, VectorSpace}; use std::{ cmp::{max, max_by, min_by}, marker::PhantomData, - ops::RangeInclusive, + ops::{Deref, RangeInclusive}, }; /// A nonempty closed interval, possibly infinite in either direction. @@ -134,6 +134,7 @@ impl Interpolable for Quat { /// An error indicating that a resampling operation could not be performed because of /// malformed inputs. +#[derive(Debug)] // TODO: Make this an actual Error. pub enum ResamplingError { /// This resampling operation was not provided with enough samples to have well-formed output. NotEnoughSamples(usize), @@ -205,7 +206,9 @@ where fn resample_uneven( &self, sample_times: impl IntoIterator, - ) -> Result, ResamplingError> { + ) -> Result, ResamplingError> + where Self: Sized + { let mut times: Vec = sample_times .into_iter() .filter(|t| t.is_finite() && self.domain().contains(*t)) @@ -335,6 +338,40 @@ where _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(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 + } +} + +impl Curve for D +where + T: Interpolable, + C: Curve + ?Sized, + D: Deref, +{ + fn domain(&self) -> Interval { + >::domain(self) + } + + fn sample(&self, t: f32) -> T { + >::sample(self, t) + } } /// A [`Curve`] which takes a constant value over its domain. @@ -855,3 +892,18 @@ where { curve.map(|(s, t)| (t, s)) } + +#[test] +fn my_test() { + let my_curve = function_curve((0.0..=1.0).try_into().unwrap(), |t| t * t + 1.0); + let samples = my_curve.by_ref().map(|x| x * 2.0).resample(100).unwrap(); + let new_curve = my_curve.map(|x| x * x); + println!("samples: {:?}", samples.samples); +} + +#[test] +fn another_test() { + let boxed_curve: Box> = Box::new(function_curve(everywhere(), |t| t * t)); + println!("size: {:?}", std::mem::size_of::> >()); + let mapped = boxed_curve.map(|x| 2.0 * x); +} \ No newline at end of file From ded34c23a6a73ad5fd81c836fd33ff076dad694b Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 18 Apr 2024 11:02:07 -0400 Subject: [PATCH 04/28] Refactored interval steps to a dedicated function --- crates/bevy_math/src/curve.rs | 58 ++++++++++++++++------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index 016a79232b5f2..b7f4e0c4b0c5f 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -3,7 +3,7 @@ use crate::{Quat, VectorSpace}; use std::{ - cmp::{max, max_by, min_by}, + cmp::{max_by, min_by}, marker::PhantomData, ops::{Deref, RangeInclusive}, }; @@ -87,6 +87,16 @@ impl Interval { let scale = other.length() / self.length(); Ok(move |x| (x - self.start) * scale + other.start) } + + /// Get an iterator over `points` equally-spaced points from this interval in increasing order. + /// Returns `None` if `points` is less than 2; the spaced points always include the endpoints. + pub fn spaced_points(self, points: usize) -> Option> { + if points < 2 { + return None; + } + let step = self.length() / (points - 1) as f32; + Some((0..points).map(move |x| self.start + x as f32 * step)) + } } impl TryFrom> for Interval { @@ -183,10 +193,12 @@ where return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); } - // When `samples` is 1, we just record the starting point, and `step` doesn't matter. - let subdivisions = max(1, samples - 1); - let step = self.domain().length() / subdivisions as f32; - let samples: Vec = (0..samples).map(|s| self.sample(s as f32 * step)).collect(); + let samples: Vec = self + .domain() + .spaced_points(samples) + .unwrap() + .map(|t| self.sample(t)) + .collect(); Ok(SampleCurve { domain: self.domain(), samples, @@ -206,8 +218,9 @@ where fn resample_uneven( &self, sample_times: impl IntoIterator, - ) -> Result, ResamplingError> - where Self: Sized + ) -> Result, ResamplingError> + where + Self: Sized, { let mut times: Vec = sample_times .into_iter() @@ -342,19 +355,21 @@ where /// 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 + /// // 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(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 { + where + Self: Sized, + { self } } @@ -454,12 +469,8 @@ where /// Like [`Curve::graph`], but with a concrete return type. pub fn graph_concrete(self) -> SampleCurve<(f32, T)> { - let subdivisions = self.samples.len() - 1; - let step = self.domain.length() / subdivisions as f32; - let times: Vec = (0..self.samples.len()) - .map(|s| self.domain.start() + (s as f32 * step)) - .collect(); - let new_samples: Vec<(f32, T)> = times.into_iter().zip(self.samples).collect(); + let times = self.domain().spaced_points(self.samples.len()).unwrap(); + let new_samples: Vec<(f32, T)> = times.zip(self.samples).collect(); SampleCurve { domain: self.domain, samples: new_samples, @@ -892,18 +903,3 @@ where { curve.map(|(s, t)| (t, s)) } - -#[test] -fn my_test() { - let my_curve = function_curve((0.0..=1.0).try_into().unwrap(), |t| t * t + 1.0); - let samples = my_curve.by_ref().map(|x| x * 2.0).resample(100).unwrap(); - let new_curve = my_curve.map(|x| x * x); - println!("samples: {:?}", samples.samples); -} - -#[test] -fn another_test() { - let boxed_curve: Box> = Box::new(function_curve(everywhere(), |t| t * t)); - println!("size: {:?}", std::mem::size_of::> >()); - let mapped = boxed_curve.map(|x| 2.0 * x); -} \ No newline at end of file From f0abd384114c93c21baa2a8675bbc21e5e64cb02 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 22 Apr 2024 07:26:51 -0400 Subject: [PATCH 05/28] Comment change --- crates/bevy_math/src/curve.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index b7f4e0c4b0c5f..4ceb80f2ce338 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -25,7 +25,7 @@ pub struct InfiniteIntervalError; impl Interval { /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be infinite - /// but cannot be empty; invalid parameters will result in an error. + /// but cannot be empty and neither endpoint can be NaN; invalid parameters will result in an error. pub fn new(start: f32, end: f32) -> Result { if start >= end || start.is_nan() || end.is_nan() { Err(InvalidIntervalError) From c7fde9e57832f4b8dea6536422b28e98f5ec2d43 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 4 May 2024 16:00:57 -0400 Subject: [PATCH 06/28] Change UnevenSampleCurve to SoA --- crates/bevy_math/src/curve.rs | 118 +++++++++++++++------------------- 1 file changed, 51 insertions(+), 67 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index 4ceb80f2ce338..4f20eea492252 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -231,8 +231,8 @@ where return Err(ResamplingError::NotEnoughSamples(times.len())); } times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); - let timed_samples = times.into_iter().map(|t| (t, self.sample(t))).collect(); - Ok(UnevenSampleCurve { timed_samples }) + let samples = times.iter().copied().map(|t| self.sample(t)).collect(); + Ok(UnevenSampleCurve { times, samples }) } /// Create a new curve by mapping the values of this curve via a function `f`; i.e., if the @@ -455,7 +455,8 @@ impl SampleCurve where T: Interpolable, { - /// Like [`Curve::map`], but with a concrete return type. + /// Like [`Curve::map`], but with a concrete return type. Unlike that function, this one is + /// not lazy, and `f` is evaluated immediately on samples to produce the result. pub fn map_concrete(self, f: impl Fn(T) -> S) -> SampleCurve where S: Interpolable, @@ -501,21 +502,6 @@ where let f = (t_shifted / step).fract(); self.samples[lower_index].interpolate(&self.samples[upper_index], f) } - - fn map(self, f: impl Fn(T) -> S) -> impl Curve - where - Self: Sized, - S: Interpolable, - { - self.map_concrete(f) - } - - fn graph(self) -> impl Curve<(f32, T)> - where - Self: Sized, - { - self.graph_concrete() - } } /// A [`Curve`] that is defined by interpolation over unevenly spaced samples. @@ -523,41 +509,42 @@ pub struct UnevenSampleCurve where T: Interpolable, { - /// The timed that make up this [`UnevenSampleCurve`] by interpolation. + /// The times for the samples of this curve. /// - /// Invariants: this must always have a length of at least 2, be sorted by time, and have no + /// Invariants: This must always have a length of at least 2, be sorted, and have no /// duplicated or non-finite times. - timed_samples: Vec<(f32, T)>, + times: Vec, + + /// The samples corresponding to the times for this curve. + /// + /// Invariants: This must always have the same length as `times`. + samples: Vec, + //timed_samples: Vec<(f32, T)>, } impl UnevenSampleCurve where T: Interpolable, { - /// Like [`Curve::map`], but with a concrete return type.. + /// Like [`Curve::map`], but with a concrete return type. Unlike that function, this one is + /// not lazy, and `f` is evaluated immediately on samples to produce the result. pub fn map_concrete(self, f: impl Fn(T) -> S) -> UnevenSampleCurve where S: Interpolable, { - let new_samples: Vec<(f32, S)> = self - .timed_samples - .into_iter() - .map(|(t, x)| (t, f(x))) - .collect(); + let new_samples: Vec = self.samples.into_iter().map(|x| f(x)).collect(); UnevenSampleCurve { - timed_samples: new_samples, + times: self.times, + samples: new_samples, } } /// Like [`Curve::graph`], but with a concrete return type. pub fn graph_concrete(self) -> UnevenSampleCurve<(f32, T)> { - let new_samples: Vec<(f32, (f32, T))> = self - .timed_samples - .into_iter() - .map(|(t, x)| (t, (t, x))) - .collect(); + let new_samples = self.times.iter().copied().zip(self.samples).collect(); UnevenSampleCurve { - timed_samples: new_samples, + times: self.times, + samples: new_samples, } } @@ -568,10 +555,16 @@ where /// The samples are resorted 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { - self.timed_samples.iter_mut().for_each(|(t, _)| *t = f(*t)); - self.timed_samples.dedup_by(|(t1, _), (t2, _)| (*t1).eq(t2)); - self.timed_samples - .sort_by(|(t1, _), (t2, _)| t1.partial_cmp(t2).unwrap()); + let mut timed_samples: Vec<(f32, T)> = self + .times + .into_iter() + .map(|t| f(t)) + .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 } } @@ -582,47 +575,34 @@ where { #[inline] fn domain(&self) -> Interval { - let start = self.timed_samples.first().unwrap().0; - let end = self.timed_samples.last().unwrap().0; - Interval::new(start, end).unwrap() + let start = self.times.first().unwrap(); + let end = self.times.last().unwrap(); + Interval::new(*start, *end).unwrap() } #[inline] fn sample(&self, t: f32) -> T { match self - .timed_samples - .binary_search_by(|(pt, _)| pt.partial_cmp(&t).unwrap()) + .times + .binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) { - Ok(index) => self.timed_samples[index].1.clone(), + Ok(index) => self.samples[index].clone(), Err(index) => { if index == 0 { - self.timed_samples.first().unwrap().1.clone() - } else if index == self.timed_samples.len() { - self.timed_samples.last().unwrap().1.clone() + self.samples.first().unwrap().clone() + } else if index == self.times.len() { + self.samples.last().unwrap().clone() } else { - let (t_lower, v_lower) = self.timed_samples.get(index - 1).unwrap(); - let (t_upper, v_upper) = self.timed_samples.get(index).unwrap(); + let t_lower = self.times[index - 1]; + let v_lower = self.samples.get(index - 1).unwrap(); + let t_upper = self.times[index]; + let v_upper = self.samples.get(index).unwrap(); let s = (t - t_lower) / (t_upper - t_lower); - v_lower.interpolate(v_upper, s) + v_lower.interpolate(&v_upper, s) } } } } - - fn map(self, f: impl Fn(T) -> S) -> impl Curve - where - Self: Sized, - S: Interpolable, - { - self.map_concrete(f) - } - - fn graph(self) -> impl Curve<(f32, T)> - where - Self: Sized, - { - self.graph_concrete() - } } /// A [`Curve`] whose samples are defined by mapping samples from another curve through a @@ -656,7 +636,7 @@ where (self.f)(self.preimage.sample(t)) } - // Specialized implementation of [`Curve::map`] that reuses data. + #[inline] fn map(self, g: impl Fn(T) -> R) -> impl Curve where Self: Sized, @@ -670,6 +650,7 @@ where } } + #[inline] fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve where Self: Sized, @@ -713,7 +694,7 @@ where self.base.sample((self.f)(t)) } - // Specialized implementation of [`Curve::reparametrize`] that reuses data. + #[inline] fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve where Self: Sized, @@ -727,6 +708,7 @@ where } } + #[inline] fn map(self, g: impl Fn(T) -> S) -> impl Curve where Self: Sized, @@ -780,6 +762,7 @@ where (self.forward_map)(self.base.sample((self.reparam_map)(t))) } + #[inline] fn map(self, g: impl Fn(T) -> R) -> impl Curve where Self: Sized, @@ -795,6 +778,7 @@ where } } + #[inline] fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve where Self: Sized, From b9e113d5951cf19e720df0d2e951c340521fd3eb Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sun, 5 May 2024 15:57:57 -0400 Subject: [PATCH 07/28] Derive Error on error types --- crates/bevy_math/src/curve.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index 4f20eea492252..aa836e59b5fa5 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -7,6 +7,7 @@ use std::{ marker::PhantomData, ops::{Deref, RangeInclusive}, }; +use thiserror::Error; /// A nonempty closed interval, possibly infinite in either direction. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] @@ -16,11 +17,13 @@ pub struct Interval { } /// An error that indicates that an operation would have returned an invalid [`Interval`]. -#[derive(Debug)] +#[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)] +#[derive(Debug, Error)] +#[error("This operation does not make sense in the context of an infinite interval")] pub struct InfiniteIntervalError; impl Interval { @@ -88,7 +91,7 @@ impl Interval { Ok(move |x| (x - self.start) * scale + other.start) } - /// Get an iterator over `points` equally-spaced points from this interval in increasing order. + /// Get an iterator over equally-spaced points from this interval in increasing order. /// Returns `None` if `points` is less than 2; the spaced points always include the endpoints. pub fn spaced_points(self, points: usize) -> Option> { if points < 2 { @@ -144,11 +147,14 @@ impl Interpolable for Quat { /// An error indicating that a resampling operation could not be performed because of /// malformed inputs. -#[derive(Debug)] // TODO: Make this an actual Error. +#[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), } From b0c5f44ba0e8acc361ac5f55c12f8b75b38d061d Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sun, 5 May 2024 16:01:43 -0400 Subject: [PATCH 08/28] Lints --- crates/bevy_math/src/curve.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index aa836e59b5fa5..bef64a498a4d2 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -538,7 +538,7 @@ where where S: Interpolable, { - let new_samples: Vec = self.samples.into_iter().map(|x| f(x)).collect(); + let new_samples: Vec = self.samples.into_iter().map(f).collect(); UnevenSampleCurve { times: self.times, samples: new_samples, @@ -561,12 +561,8 @@ where /// The samples are resorted 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { - let mut timed_samples: Vec<(f32, T)> = self - .times - .into_iter() - .map(|t| f(t)) - .zip(self.samples) - .collect(); + 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(); @@ -604,7 +600,7 @@ where let t_upper = self.times[index]; let v_upper = self.samples.get(index).unwrap(); let s = (t - t_lower) / (t_upper - t_lower); - v_lower.interpolate(&v_upper, s) + v_lower.interpolate(v_upper, s) } } } From 9083553979464801eb1a671196108651b30488b1 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 7 May 2024 10:45:28 -0400 Subject: [PATCH 09/28] Restrict Interpolable constraint to resampling methods --- crates/bevy_math/src/curve.rs | 49 +++++++---------------------------- 1 file changed, 10 insertions(+), 39 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index bef64a498a4d2..64237f75cd5a3 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -161,10 +161,7 @@ pub enum ResamplingError { /// 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 -where - T: Interpolable, -{ +pub trait Curve { /// The interval over which this curve is parametrized. fn domain(&self) -> Interval; @@ -191,7 +188,10 @@ where /// 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(&self, samples: usize) -> Result, ResamplingError> { + fn resample(&self, samples: usize) -> Result, ResamplingError> + where + T: Interpolable, + { if samples < 2 { return Err(ResamplingError::NotEnoughSamples(samples)); } @@ -227,6 +227,7 @@ where ) -> Result, ResamplingError> where Self: Sized, + T: Interpolable, { let mut times: Vec = sample_times .into_iter() @@ -247,7 +248,6 @@ where fn map(self, f: impl Fn(T) -> S) -> impl Curve where Self: Sized, - S: Interpolable, { MapCurve { preimage: self, @@ -346,7 +346,6 @@ where fn zip(self, other: C) -> Result, InvalidIntervalError> where Self: Sized, - S: Interpolable, C: Curve + Sized, { let domain = self.domain().intersect(other.domain())?; @@ -382,7 +381,6 @@ where impl Curve for D where - T: Interpolable, C: Curve + ?Sized, D: Deref, { @@ -398,7 +396,7 @@ where /// A [`Curve`] which takes a constant value over its domain. pub struct ConstantCurve where - T: Interpolable, + T: Clone, { domain: Interval, value: T, @@ -406,7 +404,7 @@ where impl Curve for ConstantCurve where - T: Interpolable, + T: Clone, { #[inline] fn domain(&self) -> Interval { @@ -422,7 +420,6 @@ where /// A [`Curve`] defined by a function. pub struct FunctionCurve where - T: Interpolable, F: Fn(f32) -> T, { domain: Interval, @@ -431,7 +428,6 @@ where impl Curve for FunctionCurve where - T: Interpolable, F: Fn(f32) -> T, { #[inline] @@ -525,7 +521,6 @@ where /// /// Invariants: This must always have the same length as `times`. samples: Vec, - //timed_samples: Vec<(f32, T)>, } impl UnevenSampleCurve @@ -611,8 +606,6 @@ where /// given function. pub struct MapCurve where - S: Interpolable, - T: Interpolable, C: Curve, F: Fn(S) -> T, { @@ -623,8 +616,6 @@ where impl Curve for MapCurve where - S: Interpolable, - T: Interpolable, C: Curve, F: Fn(S) -> T, { @@ -642,7 +633,6 @@ where fn map(self, g: impl Fn(T) -> R) -> impl Curve where Self: Sized, - R: Interpolable, { let gf = move |x| g((self.f)(x)); MapCurve { @@ -670,7 +660,6 @@ where /// A [`Curve`] whose sample space is mapped onto that of some base curve's before sampling. pub struct ReparamCurve where - T: Interpolable, C: Curve, F: Fn(f32) -> f32, { @@ -682,7 +671,6 @@ where impl Curve for ReparamCurve where - T: Interpolable, C: Curve, F: Fn(f32) -> f32, { @@ -714,7 +702,6 @@ where fn map(self, g: impl Fn(T) -> S) -> impl Curve where Self: Sized, - S: Interpolable, { MapReparamCurve { reparam_domain: self.domain, @@ -733,8 +720,6 @@ where /// itself inside new structs. pub struct MapReparamCurve where - S: Interpolable, - T: Interpolable, C: Curve, F: Fn(S) -> T, G: Fn(f32) -> f32, @@ -748,8 +733,6 @@ where impl Curve for MapReparamCurve where - S: Interpolable, - T: Interpolable, C: Curve, F: Fn(S) -> T, G: Fn(f32) -> f32, @@ -768,7 +751,6 @@ where fn map(self, g: impl Fn(T) -> R) -> impl Curve where Self: Sized, - R: Interpolable, { let gf = move |x| g((self.forward_map)(x)); MapReparamCurve { @@ -799,7 +781,6 @@ where /// A [`Curve`] that is the graph of another curve over its parameter space. pub struct GraphCurve where - T: Interpolable, C: Curve, { base: C, @@ -808,7 +789,6 @@ where impl Curve<(f32, T)> for GraphCurve where - T: Interpolable, C: Curve, { #[inline] @@ -825,8 +805,6 @@ where /// A [`Curve`] that combines the data from two constituent curves into a tuple output type. pub struct ProductCurve where - S: Interpolable, - T: Interpolable, C: Curve, D: Curve, { @@ -838,8 +816,6 @@ where impl Curve<(S, T)> for ProductCurve where - S: Interpolable, - T: Interpolable, C: Curve, D: Curve, { @@ -867,7 +843,7 @@ pub fn everywhere() -> Interval { } /// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. -pub fn constant_curve(domain: Interval, value: T) -> impl Curve { +pub fn constant_curve(domain: Interval, value: T) -> impl Curve { ConstantCurve { domain, value } } @@ -875,17 +851,12 @@ pub fn constant_curve(domain: Interval, value: T) -> impl Curve /// evaluating the function. pub fn function_curve(domain: Interval, f: F) -> impl Curve where - T: Interpolable, F: Fn(f32) -> T, { FunctionCurve { domain, f } } /// Flip a curve that outputs tuples so that the tuples are arranged the other way. -pub fn flip(curve: impl Curve<(S, T)>) -> impl Curve<(T, S)> -where - S: Interpolable, - T: Interpolable, -{ +pub fn flip(curve: impl Curve<(S, T)>) -> impl Curve<(T, S)> { curve.map(|(s, t)| (t, s)) } From 02de000a24d39c0c909b9de21083aa62e800804c Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 7 May 2024 15:11:13 -0400 Subject: [PATCH 10/28] Address early review comments --- crates/bevy_math/src/curve.rs | 52 +++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index 64237f75cd5a3..20a88289be527 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -26,6 +26,19 @@ pub struct InvalidIntervalError; #[error("This operation does not make sense in the context of an infinite interval")] pub struct InfiniteIntervalError; +/// An error indicating that spaced points on an interval could not be formed. +#[derive(Debug, Error)] +#[error("Could not sample evenly-spaced points with these inputs")] +pub enum SpacedPointsError { + /// This operation failed because fewer than two points were requested. + #[error("Parameter `points` must be at least 2")] + NotEnoughPoints, + + /// This operation failed because the underlying interval is unbounded. + #[error("Cannot sample evenly-spaced points on an infinite interval")] + InfiniteInterval(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. @@ -93,12 +106,18 @@ impl Interval { /// Get an iterator over equally-spaced points from this interval in increasing order. /// Returns `None` if `points` is less than 2; the spaced points always include the endpoints. - pub fn spaced_points(self, points: usize) -> Option> { + pub fn spaced_points( + self, + points: usize, + ) -> Result, SpacedPointsError> { if points < 2 { - return None; + return Err(SpacedPointsError::NotEnoughPoints); + } + if !self.is_finite() { + return Err(SpacedPointsError::InfiniteInterval(InfiniteIntervalError)); } let step = self.length() / (points - 1) as f32; - Some((0..points).map(move |x| self.start + x as f32 * step)) + Ok((0..points).map(move |x| self.start + x as f32 * step)) } } @@ -153,6 +172,7 @@ 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), @@ -492,17 +512,27 @@ where #[inline] fn sample(&self, t: f32) -> T { - // We clamp `t` to the domain. - let t = self.domain.clamp(t); - - // Inside the curve itself, interpolate between the two nearest sample values. + // Inside the curve itself, we interpolate between the two nearest sample values. let subdivs = self.samples.len() - 1; let step = self.domain.length() / subdivs as f32; let t_shifted = t - self.domain.start(); - let lower_index = (t_shifted / step).floor() as usize; - let upper_index = (t_shifted / step).ceil() as usize; - let f = (t_shifted / step).fract(); - self.samples[lower_index].interpolate(&self.samples[upper_index], f) + let steps_taken = t_shifted / step; + + // Using `steps_taken` as the source of truth, clamp to the range of valid indices. + if steps_taken <= 0.0 { + self.samples.first().unwrap().clone() + } else if steps_taken >= (self.samples.len() - 1) as f32 { + self.samples.last().unwrap().clone() + } else { + // Here we use only the floor and the fractional part of `steps_taken` to interpolate + // between the two nearby sample points; `lower_index + 1` is known to be a valid index + // because otherwise, `steps_taken.floor()` must be at least `self.samples.len() - 1`, + // but the previous branch captures all such values. + let lower_index = steps_taken.floor() as usize; + let upper_index = lower_index + 1; + let fract = steps_taken.fract(); + self.samples[lower_index].interpolate(&self.samples[upper_index], fract) + } } } From 6ea34166c6de130f70828ba2e468f808dd4b5904 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 8 May 2024 06:49:49 -0400 Subject: [PATCH 11/28] Explicitly clamp index in UnevenSampleCurve::sample --- crates/bevy_math/src/curve.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/bevy_math/src/curve.rs b/crates/bevy_math/src/curve.rs index 20a88289be527..a0edc4e59ff74 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve.rs @@ -525,10 +525,11 @@ where self.samples.last().unwrap().clone() } else { // Here we use only the floor and the fractional part of `steps_taken` to interpolate - // between the two nearby sample points; `lower_index + 1` is known to be a valid index - // because otherwise, `steps_taken.floor()` must be at least `self.samples.len() - 1`, - // but the previous branch captures all such values. + // between the two nearby sample points. let lower_index = steps_taken.floor() as usize; + + // Explicitly clamp the lower index just in case. + let lower_index = lower_index.min(self.samples.len() - 2); let upper_index = lower_index + 1; let fract = steps_taken.fract(); self.samples[lower_index].interpolate(&self.samples[upper_index], fract) From 36476b11d84c4f0f6d13143c3e3c99d580b6ac2e Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 8 May 2024 12:55:56 -0400 Subject: [PATCH 12/28] Reorganized, added some tests --- crates/bevy_math/src/curve/interpolable.rs | 39 ++ crates/bevy_math/src/curve/interval.rs | 298 ++++++++++++++++ .../bevy_math/src/{curve.rs => curve/mod.rs} | 334 +++++++++--------- 3 files changed, 496 insertions(+), 175 deletions(-) create mode 100644 crates/bevy_math/src/curve/interpolable.rs create mode 100644 crates/bevy_math/src/curve/interval.rs rename crates/bevy_math/src/{curve.rs => curve/mod.rs} (77%) diff --git a/crates/bevy_math/src/curve/interpolable.rs b/crates/bevy_math/src/curve/interpolable.rs new file mode 100644 index 0000000000000..9458e1528c8e6 --- /dev/null +++ b/crates/bevy_math/src/curve/interpolable.rs @@ -0,0 +1,39 @@ +//! The [`Interpolable`] trait for types that support interpolation between two values. + +use crate::{Quat, VectorSpace}; + +/// A trait for types whose values can be intermediately interpolated between two given values +/// with an auxiliary parameter. +pub trait Interpolable: Clone { + /// Interpolate between this value and the `other` given value using the parameter `t`. + /// Note that the parameter `t` is not necessarily clamped to lie between `0` and `1`. + fn interpolate(&self, other: &Self, t: f32) -> Self; +} + +impl Interpolable for (S, T) +where + S: Interpolable, + T: Interpolable, +{ + fn interpolate(&self, other: &Self, t: f32) -> Self { + ( + self.0.interpolate(&other.0, t), + self.1.interpolate(&other.1, t), + ) + } +} + +impl Interpolable for T +where + T: VectorSpace, +{ + fn interpolate(&self, other: &Self, t: f32) -> Self { + self.lerp(*other, t) + } +} + +impl Interpolable for Quat { + fn interpolate(&self, other: &Self, t: f32) -> Self { + self.slerp(*other, t) + } +} diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs new file mode 100644 index 0000000000000..72d8993e7be1d --- /dev/null +++ b/crates/bevy_math/src/curve/interval.rs @@ -0,0 +1,298 @@ +//! The [`Interval`] type for nonempty intervals used by the [`Curve`](super::Curve) trait. + +use std::{ + cmp::{max_by, min_by}, + ops::RangeInclusive, +}; +use thiserror::Error; + +/// A nonempty closed interval, possibly infinite in either direction. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +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; + +/// An error indicating that spaced points on an interval could not be formed. +#[derive(Debug, Error)] +#[error("Could not sample evenly-spaced points with these inputs")] +pub enum SpacedPointsError { + /// This operation failed because fewer than two points were requested. + #[error("Parameter `points` must be at least 2")] + NotEnoughPoints, + + /// This operation failed because the underlying interval is unbounded. + #[error("Cannot sample evenly-spaced points on an infinite interval")] + InfiniteInterval(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. + 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 this interval is finite. + #[inline] + pub fn is_finite(self) -> bool { + self.length().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) + } + + /// Clamp the given `value` to lie within this interval. + #[inline] + pub fn clamp(self, value: f32) -> f32 { + value.clamp(self.start, self.end) + } + + /// Get the linear map which maps this curve onto the `other` one. Returns an error if either + /// interval is infinite. + pub 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) + } + + /// Get an iterator over equally-spaced points from this interval in increasing order. + /// Returns `None` if `points` is less than 2; the spaced points always include the endpoints. + pub fn spaced_points( + self, + points: usize, + ) -> Result, SpacedPointsError> { + if points < 2 { + return Err(SpacedPointsError::NotEnoughPoints); + } + if !self.is_finite() { + return Err(SpacedPointsError::InfiniteInterval(InfiniteIntervalError)); + } + let step = self.length() / (points - 1) as f32; + Ok((0..points).map(move |x| self.start + x as f32 * step)) + } +} + +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`]. +pub fn interval(start: f32, end: f32) -> Result { + Interval::new(start, end) +} + +/// The [`Interval`] from negative infinity to infinity. +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 = ivl.spaced_points(1); + assert!(points_iter.is_err()); + 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.rs b/crates/bevy_math/src/curve/mod.rs similarity index 77% rename from crates/bevy_math/src/curve.rs rename to crates/bevy_math/src/curve/mod.rs index a0edc4e59ff74..97a66eb0d3ec5 100644 --- a/crates/bevy_math/src/curve.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -1,168 +1,13 @@ -//! Houses the [`Curve`] trait together with the [`Interpolable`] trait and the [`Interval`] -//! struct that it depends on. - -use crate::{Quat, VectorSpace}; -use std::{ - cmp::{max_by, min_by}, - marker::PhantomData, - ops::{Deref, RangeInclusive}, -}; -use thiserror::Error; - -/// A nonempty closed interval, possibly infinite in either direction. -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -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; - -/// An error indicating that spaced points on an interval could not be formed. -#[derive(Debug, Error)] -#[error("Could not sample evenly-spaced points with these inputs")] -pub enum SpacedPointsError { - /// This operation failed because fewer than two points were requested. - #[error("Parameter `points` must be at least 2")] - NotEnoughPoints, - - /// This operation failed because the underlying interval is unbounded. - #[error("Cannot sample evenly-spaced points on an infinite interval")] - InfiniteInterval(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. - 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 this interval is finite. - #[inline] - pub fn is_finite(self) -> bool { - self.length().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) - } - - /// Clamp the given `value` to lie within this interval. - #[inline] - pub fn clamp(self, value: f32) -> f32 { - value.clamp(self.start, self.end) - } - - /// Get the linear map which maps this curve onto the `other` one. Returns an error if either - /// interval is infinite. - pub 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) - } - - /// Get an iterator over equally-spaced points from this interval in increasing order. - /// Returns `None` if `points` is less than 2; the spaced points always include the endpoints. - pub fn spaced_points( - self, - points: usize, - ) -> Result, SpacedPointsError> { - if points < 2 { - return Err(SpacedPointsError::NotEnoughPoints); - } - if !self.is_finite() { - return Err(SpacedPointsError::InfiniteInterval(InfiniteIntervalError)); - } - let step = self.length() / (points - 1) as f32; - Ok((0..points).map(move |x| self.start + x as f32 * step)) - } -} - -impl TryFrom> for Interval { - type Error = InvalidIntervalError; - fn try_from(range: RangeInclusive) -> Result { - Interval::new(*range.start(), *range.end()) - } -} +//! The [`Curve`] trait, used to describe curves in a number of different domains. This module also +//! contains the [`Interpolable`] trait and the [`Interval`] type. -/// A trait for types whose values can be intermediately interpolated between two given values -/// with an auxiliary parameter. -pub trait Interpolable: Clone { - /// Interpolate between this value and the `other` given value using the parameter `t`. - /// Note that the parameter `t` is not necessarily clamped to lie between `0` and `1`. - fn interpolate(&self, other: &Self, t: f32) -> Self; -} +pub mod interpolable; +pub mod interval; -impl Interpolable for (S, T) -where - S: Interpolable, - T: Interpolable, -{ - fn interpolate(&self, other: &Self, t: f32) -> Self { - ( - self.0.interpolate(&other.0, t), - self.1.interpolate(&other.1, t), - ) - } -} - -impl Interpolable for T -where - T: VectorSpace, -{ - fn interpolate(&self, other: &Self, t: f32) -> Self { - self.lerp(*other, t) - } -} - -impl Interpolable for Quat { - fn interpolate(&self, other: &Self, t: f32) -> Self { - self.slerp(*other, t) - } -} +use interpolable::Interpolable; +use interval::*; +use std::{marker::PhantomData, ops::Deref}; +use thiserror::Error; /// An error indicating that a resampling operation could not be performed because of /// malformed inputs. @@ -861,18 +706,6 @@ where } } -// Library functions: - -/// Create an [`Interval`] with a given `start` and `end`. Alias of [`Interval::new`]. -pub fn interval(start: f32, end: f32) -> Result { - Interval::new(start, end) -} - -/// The [`Interval`] from negative infinity to infinity. -pub fn everywhere() -> Interval { - Interval::new(f32::NEG_INFINITY, f32::INFINITY).unwrap() -} - /// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. pub fn constant_curve(domain: Interval, value: T) -> impl Curve { ConstantCurve { domain, value } @@ -891,3 +724,154 @@ where pub fn flip(curve: impl Curve<(S, T)>) -> impl Curve<(T, S)> { curve.map(|(s, t)| (t, s)) } + +#[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) == true); + 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(), |t| t.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(|x| Quat::from_rotation_z(x)); + 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(), |t| t.log2()); + let reparametrized_curve = curve + .by_ref() + .reparametrize(interval(0.0, f32::INFINITY).unwrap(), |t| t.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(), |t| t.exp2()); + let first_mapped = curve.map(|x| x.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(), |t| t.exp2()); + let first_reparam = curve.reparametrize(interval(1.0, 2.0).unwrap(), |t| t.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(), |t| t.log2()); + + // Need at least two points to sample. + let nice_try = curve.by_ref().resample(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(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(), |t| t.cos()); + let resampled_curve = curve.by_ref().resample(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(), |t| t.exp()); + + // Need at least two points to resample. + let nice_try = curve.by_ref().resample_uneven([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).into_iter().map(|idx| idx as f32 * 0.1); + let resampled_curve = curve.by_ref().resample_uneven(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(), |t| t.log2()); + let sample_points = (0..10).into_iter().map(|idx| (idx as f32).exp2()); + let resampled_curve = curve.by_ref().resample_uneven(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 715fdcaf6024eb861f30b2e8818a27e55010b0e1 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 8 May 2024 13:11:06 -0400 Subject: [PATCH 13/28] Fixes to docs/lints, some re-exports --- crates/bevy_math/src/curve/mod.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 97a66eb0d3ec5..d1fbf05a17bb8 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -4,8 +4,10 @@ pub mod interpolable; pub mod interval; -use interpolable::Interpolable; -use interval::*; +pub use interpolable::Interpolable; +pub use interval::{everywhere, interval, Interval}; + +use interval::{InfiniteIntervalError, InvalidIntervalError}; use std::{marker::PhantomData, ops::Deref}; use thiserror::Error; @@ -738,7 +740,7 @@ mod tests { assert!(curve.sample(-35.0) == 5.0); let curve = constant_curve(interval(0.0, 1.0).unwrap(), true); - assert!(curve.sample(2.0) == true); + assert!(curve.sample(2.0)); assert!(curve.sample_checked(2.0).is_none()); } @@ -763,7 +765,7 @@ mod tests { assert_eq!(mapped_curve.domain(), everywhere()); let curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t * TAU); - let mapped_curve = curve.map(|x| Quat::from_rotation_z(x)); + 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()); @@ -852,7 +854,7 @@ mod tests { // 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).into_iter().map(|idx| idx as f32 * 0.1); + let sample_points = (0..100).map(|idx| idx as f32 * 0.1); let resampled_curve = curve.by_ref().resample_uneven(sample_points).unwrap(); for idx in 0..100 { let test_pt = idx as f32 * 0.1; @@ -864,7 +866,7 @@ mod tests { // Another example. let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), |t| t.log2()); - let sample_points = (0..10).into_iter().map(|idx| (idx as f32).exp2()); + let sample_points = (0..10).map(|idx| (idx as f32).exp2()); let resampled_curve = curve.by_ref().resample_uneven(sample_points).unwrap(); for idx in 0..10 { let test_pt = (idx as f32).exp2(); From 65d85c8e38feef834c9ba2b0803ade1e3b0b6edf Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 21 May 2024 10:09:08 -0400 Subject: [PATCH 14/28] Add derived traits to structs --- crates/bevy_math/src/curve/interval.rs | 1 + crates/bevy_math/src/curve/mod.rs | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index 72d8993e7be1d..37adc47ec3066 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -8,6 +8,7 @@ use thiserror::Error; /// A nonempty closed interval, possibly infinite in either direction. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct Interval { start: f32, end: f32, diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index d1fbf05a17bb8..f9c76c819a206 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -261,6 +261,8 @@ where } /// A [`Curve`] which takes a constant value over its domain. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct ConstantCurve where T: Clone, @@ -285,6 +287,7 @@ where } /// A [`Curve`] defined by a function. +#[derive(Clone, Debug)] pub struct FunctionCurve where F: Fn(f32) -> T, @@ -309,6 +312,8 @@ where } /// 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))] pub struct SampleCurve where T: Interpolable, @@ -385,6 +390,8 @@ where } /// A [`Curve`] that is defined by interpolation over unevenly spaced samples. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct UnevenSampleCurve where T: Interpolable, @@ -482,6 +489,7 @@ where /// A [`Curve`] whose samples are defined by mapping samples from another curve through a /// given function. +#[derive(Clone, Debug)] pub struct MapCurve where C: Curve, @@ -536,6 +544,7 @@ where } /// A [`Curve`] whose sample space is mapped onto that of some base curve's before sampling. +#[derive(Clone, Debug)] pub struct ReparamCurve where C: Curve, @@ -596,6 +605,7 @@ where /// /// Briefly, the point is that the curve just absorbs new functions instead of rebasing /// itself inside new structs. +#[derive(Clone, Debug)] pub struct MapReparamCurve where C: Curve, @@ -657,6 +667,8 @@ where } /// 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))] pub struct GraphCurve where C: Curve, @@ -681,6 +693,8 @@ where } /// 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))] pub struct ProductCurve where C: Curve, From 6cd1d4bb85be99a1e23b53b83bf75d7d402e7deb Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 5 Jun 2024 16:54:25 -0400 Subject: [PATCH 15/28] Add explicit interpolation --- crates/bevy_math/src/curve/mod.rs | 233 ++++++++++++++++++++++++++---- 1 file changed, 204 insertions(+), 29 deletions(-) diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index f9c76c819a206..b790b7c503c79 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -55,7 +55,7 @@ pub trait Curve { /// 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(&self, samples: usize) -> Result, ResamplingError> + fn resample_auto(&self, samples: usize) -> Result, ResamplingError> where T: Interpolable, { @@ -66,6 +66,39 @@ pub trait Curve { return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); } + let samples: Vec = self + .domain() + .spaced_points(samples) + .unwrap() + .map(|t| self.sample(t)) + .collect(); + Ok(SampleAutoCurve { + domain: self.domain(), + samples, + }) + } + + /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally + /// spaced values, using the provided `interpolation` to interpolate between adjacent samples. + /// A total of `samples` samples are used, although at least two samples are required 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( + &self, + samples: usize, + interpolation: I, + ) -> Result, ResamplingError> + where + Self: Sized, + I: Fn(&T, &T, f32) -> T, + { + 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) @@ -75,23 +108,24 @@ pub trait Curve { Ok(SampleCurve { domain: self.domain(), 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 range. + /// two valid times within the curve's domain interval. /// - /// Irredundant sample times, non-finite sample times, and sample times outside of the domain + /// 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 [`UnevenSampleCurve`] stretches between the first and last + /// The domain of the produced [`UnevenSampleAutoCurve`] stretches between the first and last /// sample times of the iterator. - fn resample_uneven( + fn resample_uneven_auto( &self, sample_times: impl IntoIterator, - ) -> Result, ResamplingError> + ) -> Result, ResamplingError> where Self: Sized, T: Interpolable, @@ -106,7 +140,44 @@ pub trait Curve { } times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); let samples = times.iter().copied().map(|t| self.sample(t)).collect(); - Ok(UnevenSampleCurve { times, samples }) + Ok(UnevenSampleAutoCurve { times, samples }) + } + + /// 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. + 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 { + times, + samples, + interpolation, + }) } /// Create a new curve by mapping the values of this curve via a function `f`; i.e., if the @@ -314,7 +385,7 @@ where /// 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))] -pub struct SampleCurve +pub struct SampleAutoCurve where T: Interpolable, { @@ -325,35 +396,35 @@ where samples: Vec, } -impl SampleCurve +impl SampleAutoCurve where T: Interpolable, { /// Like [`Curve::map`], but with a concrete return type. Unlike that function, this one is /// not lazy, and `f` is evaluated immediately on samples to produce the result. - pub fn map_concrete(self, f: impl Fn(T) -> S) -> SampleCurve + pub fn map_concrete(self, f: impl Fn(T) -> S) -> SampleAutoCurve where S: Interpolable, { let new_samples: Vec = self.samples.into_iter().map(f).collect(); - SampleCurve { + SampleAutoCurve { domain: self.domain, samples: new_samples, } } /// Like [`Curve::graph`], but with a concrete return type. - pub fn graph_concrete(self) -> SampleCurve<(f32, T)> { + pub fn graph_concrete(self) -> SampleAutoCurve<(f32, T)> { let times = self.domain().spaced_points(self.samples.len()).unwrap(); let new_samples: Vec<(f32, T)> = times.zip(self.samples).collect(); - SampleCurve { + SampleAutoCurve { domain: self.domain, samples: new_samples, } } } -impl Curve for SampleCurve +impl Curve for SampleAutoCurve where T: Interpolable, { @@ -389,10 +460,61 @@ where } } +/// A [`Curve`] that is defined by explicit neighbor interpolation over a set of samples. +pub struct SampleCurve { + domain: Interval, + /// The samples that make up this curve by interpolation. + /// + /// Invariant: this must always have a length of at least 2. + samples: Vec, + interpolation: I, +} + +impl Curve for SampleCurve +where + T: Clone, + I: Fn(&T, &T, f32) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample(&self, t: f32) -> T { + // Inside the curve itself, we interpolate between the two nearest sample values. + let subdivs = self.samples.len() - 1; + let step = self.domain.length() / subdivs as f32; + let t_shifted = t - self.domain.start(); + let steps_taken = t_shifted / step; + + // Using `steps_taken` as the source of truth, clamp to the range of valid indices. + if steps_taken <= 0.0 { + self.samples.first().unwrap().clone() + } else if steps_taken >= (self.samples.len() - 1) as f32 { + self.samples.last().unwrap().clone() + } else { + // Here we use only the floor and the fractional part of `steps_taken` to interpolate + // between the two nearby sample points. + let lower_index = steps_taken.floor() as usize; + + // Explicitly clamp the lower index just in case. + let lower_index = lower_index.min(self.samples.len() - 2); + let upper_index = lower_index + 1; + let fract = steps_taken.fract(); + (self.interpolation)( + &self.samples[lower_index], + &self.samples[upper_index], + fract, + ) + } + } +} + /// A [`Curve`] that is defined by interpolation over unevenly spaced samples. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct UnevenSampleCurve +pub struct UnevenSampleAutoCurve where T: Interpolable, { @@ -408,39 +530,39 @@ where samples: Vec, } -impl UnevenSampleCurve +impl UnevenSampleAutoCurve where T: Interpolable, { /// Like [`Curve::map`], but with a concrete return type. Unlike that function, this one is /// not lazy, and `f` is evaluated immediately on samples to produce the result. - pub fn map_concrete(self, f: impl Fn(T) -> S) -> UnevenSampleCurve + pub fn map_concrete(self, f: impl Fn(T) -> S) -> UnevenSampleAutoCurve where S: Interpolable, { let new_samples: Vec = self.samples.into_iter().map(f).collect(); - UnevenSampleCurve { + UnevenSampleAutoCurve { times: self.times, samples: new_samples, } } /// Like [`Curve::graph`], but with a concrete return type. - pub fn graph_concrete(self) -> UnevenSampleCurve<(f32, T)> { + pub fn graph_concrete(self) -> UnevenSampleAutoCurve<(f32, T)> { let new_samples = self.times.iter().copied().zip(self.samples).collect(); - UnevenSampleCurve { + UnevenSampleAutoCurve { times: self.times, samples: new_samples, } } - /// This [`UnevenSampleCurve`], but with the sample times moved by the map `f`. + /// 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 resorted 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { + pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleAutoCurve { 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)); @@ -451,7 +573,7 @@ where } } -impl Curve for UnevenSampleCurve +impl Curve for UnevenSampleAutoCurve where T: Interpolable, { @@ -487,6 +609,59 @@ where } } +/// A [`Curve`] that is defined by interpolation over unevenly spaced samples with explicit +/// interpolation. +pub struct UnevenSampleCurve { + /// 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. + times: Vec, + + /// The samples corresponding to the times for this curve. + /// + /// Invariants: This must always have the same length as `times`. + samples: Vec, + interpolation: I, +} + +impl Curve for UnevenSampleCurve +where + T: Clone, + I: Fn(&T, &T, f32) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + let start = self.times.first().unwrap(); + let end = self.times.last().unwrap(); + Interval::new(*start, *end).unwrap() + } + + #[inline] + fn sample(&self, t: f32) -> T { + match self + .times + .binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) + { + Ok(index) => self.samples[index].clone(), + Err(index) => { + if index == 0 { + self.samples.first().unwrap().clone() + } else if index == self.times.len() { + self.samples.last().unwrap().clone() + } else { + let t_lower = self.times[index - 1]; + let v_lower = self.samples.get(index - 1).unwrap(); + let t_upper = self.times[index]; + let v_upper = self.samples.get(index).unwrap(); + let s = (t - t_lower) / (t_upper - t_lower); + (self.interpolation)(v_lower, v_upper, s) + } + } + } + } +} + /// A [`Curve`] whose samples are defined by mapping samples from another curve through a /// given function. #[derive(Clone, Debug)] @@ -833,13 +1008,13 @@ mod tests { let curve = function_curve(interval(1.0, 4.0).unwrap(), |t| t.log2()); // Need at least two points to sample. - let nice_try = curve.by_ref().resample(1); + 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(101).unwrap(); + 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; @@ -849,7 +1024,7 @@ mod tests { // Another example. let curve = function_curve(interval(0.0, TAU).unwrap(), |t| t.cos()); - let resampled_curve = curve.by_ref().resample(1001).unwrap(); + 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; @@ -863,13 +1038,13 @@ mod tests { let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), |t| t.exp()); // Need at least two points to resample. - let nice_try = curve.by_ref().resample_uneven([1.0; 1]); + 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(sample_points).unwrap(); + 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); @@ -881,7 +1056,7 @@ mod tests { // Another example. let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), |t| t.log2()); let sample_points = (0..10).map(|idx| (idx as f32).exp2()); - let resampled_curve = curve.by_ref().resample_uneven(sample_points).unwrap(); + 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); From 5f83f25d8bdb9d14d71bc708cde7d71b1cad0ca3 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 6 Jun 2024 18:12:06 -0400 Subject: [PATCH 16/28] Move to explicitly interpolated sampling --- crates/bevy_math/src/curve/interval.rs | 2 +- crates/bevy_math/src/curve/mod.rs | 171 +++++++++++++++++-------- 2 files changed, 120 insertions(+), 53 deletions(-) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index 37adc47ec3066..0a1d8b2739c94 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -103,7 +103,7 @@ impl Interval { } /// Get an iterator over equally-spaced points from this interval in increasing order. - /// Returns `None` if `points` is less than 2; the spaced points always include the endpoints. + /// Returns an error if `points` is less than 2 or if the interval is unbounded. pub fn spaced_points( self, points: usize, diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index b790b7c503c79..6bdc63f8d1139 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -83,6 +83,19 @@ pub trait Curve { /// A total of `samples` samples are used, although at least two samples are required 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. + /// + /// 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| Rotation2d::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, samples: usize, @@ -112,6 +125,25 @@ pub trait Curve { }) } + /// 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> { + 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 the given set of times. The given `sample_times` are expected to contain at least /// two valid times within the curve's domain interval. @@ -154,6 +186,10 @@ pub trait Curve { /// /// 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, @@ -305,7 +341,7 @@ pub trait 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(100).unwrap(); + /// 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(); /// ``` @@ -396,34 +432,6 @@ where samples: Vec, } -impl SampleAutoCurve -where - T: Interpolable, -{ - /// Like [`Curve::map`], but with a concrete return type. Unlike that function, this one is - /// not lazy, and `f` is evaluated immediately on samples to produce the result. - pub fn map_concrete(self, f: impl Fn(T) -> S) -> SampleAutoCurve - where - S: Interpolable, - { - let new_samples: Vec = self.samples.into_iter().map(f).collect(); - SampleAutoCurve { - domain: self.domain, - samples: new_samples, - } - } - - /// Like [`Curve::graph`], but with a concrete return type. - pub fn graph_concrete(self) -> SampleAutoCurve<(f32, T)> { - let times = self.domain().spaced_points(self.samples.len()).unwrap(); - let new_samples: Vec<(f32, T)> = times.zip(self.samples).collect(); - SampleAutoCurve { - domain: self.domain, - samples: new_samples, - } - } -} - impl Curve for SampleAutoCurve where T: Interpolable, @@ -511,6 +519,38 @@ where } } +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, + { + let samples: Vec = samples.into(); + if samples.len() < 2 { + return Err(ResamplingError::NotEnoughSamples(samples.len())); + } + if !domain.is_finite() { + return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); + } + + Ok(SampleCurve { + domain, + samples, + interpolation, + }) + } +} + /// A [`Curve`] that is defined by interpolation over unevenly spaced samples. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -534,33 +574,11 @@ impl UnevenSampleAutoCurve where T: Interpolable, { - /// Like [`Curve::map`], but with a concrete return type. Unlike that function, this one is - /// not lazy, and `f` is evaluated immediately on samples to produce the result. - pub fn map_concrete(self, f: impl Fn(T) -> S) -> UnevenSampleAutoCurve - where - S: Interpolable, - { - let new_samples: Vec = self.samples.into_iter().map(f).collect(); - UnevenSampleAutoCurve { - times: self.times, - samples: new_samples, - } - } - - /// Like [`Curve::graph`], but with a concrete return type. - pub fn graph_concrete(self) -> UnevenSampleAutoCurve<(f32, T)> { - let new_samples = self.times.iter().copied().zip(self.samples).collect(); - UnevenSampleAutoCurve { - times: self.times, - samples: new_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 resorted by time after mapping and deduplicated by output time, so + /// 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleAutoCurve { let mut timed_samples: Vec<(f32, T)> = @@ -662,6 +680,55 @@ where } } +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 { + let mut timed_samples: Vec<(f32, T)> = timed_samples.into(); + // Use default Equal to not do anything in case NAN appears; it will get removed in the + // next step anyway. + timed_samples + .sort_by(|(t0, _), (t1, _)| t0.partial_cmp(t1).unwrap_or(std::cmp::Ordering::Equal)); + let (times, samples): (Vec, Vec) = timed_samples + .into_iter() + .filter(|(t, _)| t.is_finite()) + .unzip(); + if times.len() < 2 { + return Err(ResamplingError::NotEnoughSamples(times.len())); + } + Ok(UnevenSampleCurve { + times, + 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { + 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 + } +} + /// A [`Curve`] whose samples are defined by mapping samples from another curve through a /// given function. #[derive(Clone, Debug)] From 83d744f63a275852c6672586a01ef37ea9502a67 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 10 Jun 2024 07:25:54 -0400 Subject: [PATCH 17/28] Refactor SampleCurve/UnevenSampleCurve into core builders --- crates/bevy_math/src/curve/builders.rs | 285 +++++++++++++++++ crates/bevy_math/src/curve/mod.rs | 408 +++++++++---------------- 2 files changed, 428 insertions(+), 265 deletions(-) create mode 100644 crates/bevy_math/src/curve/builders.rs diff --git a/crates/bevy_math/src/curve/builders.rs b/crates/bevy_math/src/curve/builders.rs new file mode 100644 index 0000000000000..9af37df0ce43f --- /dev/null +++ b/crates/bevy_math/src/curve/builders.rs @@ -0,0 +1,285 @@ +//! Core data structures to be used internally in Curve implementations. + +use super::interval::Interval; +use thiserror::Error; + +/// 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]: SampleCore::new +/// [`domain`]: SampleCore::domain +/// [`sample_with`]: SampleCore::sample_with +/// +/// # Example +/// ```rust +/// # use bevy_math::curve::*; +/// # use bevy_math::curve::builders::*; +/// 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: SampleCore, +/// 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)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct SampleCore { + /// 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 [`SampleCore`] could not be constructed. +#[derive(Debug, Error)] +pub enum SampleCoreError { + /// Not enough samples were provided. + #[error("Need at least two samples to create a SampleCore, but {samples} were provided")] + NotEnoughSamples { + /// The number of samples that were provided. + samples: usize, + }, + + /// Unbounded domains are not compatible with `SampleCore`. + #[error("Cannot create a SampleCore over a domain with an infinite endpoint")] + InfiniteDomain, +} + +impl SampleCore { + /// Create a new [`SampleCore`] 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(SampleCoreError::NotEnoughSamples { + samples: samples.len(), + }); + } + if !domain.is_finite() { + return Err(SampleCoreError::InfiniteDomain); + } + + Ok(SampleCore { 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, + { + // Inside the curve itself, we interpolate between the two nearest sample values. + let subdivs = self.samples.len() - 1; + let step = self.domain.length() / subdivs as f32; + let t_shifted = t - self.domain.start(); + let steps_taken = t_shifted / step; + + // Using `steps_taken` as the source of truth, clamp to the range of valid indices. + if steps_taken <= 0.0 { + self.samples.first().unwrap().clone() + } else if steps_taken >= (self.samples.len() - 1) as f32 { + self.samples.last().unwrap().clone() + } else { + // Here we use only the floor and the fractional part of `steps_taken` to interpolate + // between the two nearby sample points. + let lower_index = steps_taken.floor() as usize; + + // Explicitly clamp the lower index just in case. + let lower_index = lower_index.min(self.samples.len() - 2); + let upper_index = lower_index + 1; + let fract = steps_taken.fract(); + interpolation( + &self.samples[lower_index], + &self.samples[upper_index], + fract, + ) + } + } +} + +/// 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`]. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct UnevenSampleCore { + /// 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 [`UnevenSampleCore`] could not be constructed. +#[derive(Debug, Error)] +pub enum UnevenSampleCoreError { + /// Not enough samples were provided. + #[error( + "Need at least two samples to create an UnevenSampleCore, but {samples} were provided" + )] + NotEnoughSamples { + /// The number of samples that were provided. + samples: usize, + }, +} + +impl UnevenSampleCore { + /// Create a new [`UnevenSampleCore`] 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>) -> Result { + let mut timed_samples: Vec<(f32, T)> = timed_samples.into(); + // Use default Equal to not do anything in case NAN appears; it will get removed in the + // next step anyway. + timed_samples + .sort_by(|(t0, _), (t1, _)| t0.partial_cmp(t1).unwrap_or(std::cmp::Ordering::Equal)); + let (times, samples): (Vec, Vec) = timed_samples + .into_iter() + .filter(|(t, _)| t.is_finite()) + .unzip(); + if times.len() < 2 { + return Err(UnevenSampleCoreError::NotEnoughSamples { + samples: times.len(), + }); + } + Ok(UnevenSampleCore { 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 self + .times + .binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) + { + Ok(index) => self.samples[index].clone(), + Err(index) => { + if index == 0 { + self.samples.first().unwrap().clone() + } else if index == self.times.len() { + self.samples.last().unwrap().clone() + } else { + let t_lower = self.times[index - 1]; + let v_lower = self.samples.get(index - 1).unwrap(); + let t_upper = self.times[index]; + let v_upper = self.samples.get(index).unwrap(); + let s = (t - t_lower) / (t_upper - t_lower); + interpolation(v_lower, v_upper, s) + } + } + } + } + + /// 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. + pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCore { + 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 taken more than one at a time. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct ChunkedUnevenSampleCore { + times: Vec, + samples_serial: Vec, + chunk_width: usize, +} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 6bdc63f8d1139..4d8df0d4611b9 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -1,12 +1,14 @@ //! The [`Curve`] trait, used to describe curves in a number of different domains. This module also //! contains the [`Interpolable`] trait and the [`Interval`] type. +pub mod builders; pub mod interpolable; pub mod interval; pub use interpolable::Interpolable; pub use interval::{everywhere, interval, Interval}; +use builders::{SampleCore, SampleCoreError, UnevenSampleCore, UnevenSampleCoreError}; use interval::{InfiniteIntervalError, InvalidIntervalError}; use std::{marker::PhantomData, ops::Deref}; use thiserror::Error; @@ -51,33 +53,6 @@ pub trait Curve { self.sample(t) } - /// 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: Interpolable, - { - 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 { - domain: self.domain(), - samples, - }) - } - /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally /// spaced values, using the provided `interpolation` to interpolate between adjacent samples. /// A total of `samples` samples are used, although at least two samples are required to produce @@ -119,12 +94,43 @@ pub trait Curve { .map(|t| self.sample(t)) .collect(); Ok(SampleCurve { - domain: self.domain(), - samples, + core: SampleCore { + 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: Interpolable, + { + 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: SampleCore { + 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> { @@ -145,22 +151,28 @@ pub trait Curve { } /// 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. + /// 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 [`UnevenSampleAutoCurve`] stretches between the first and last - /// sample times of the iterator. - fn resample_uneven_auto( + /// 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, - ) -> Result, ResamplingError> + interpolation: I, + ) -> Result, ResamplingError> where Self: Sized, - T: Interpolable, + I: Fn(&T, &T, f32) -> T, { let mut times: Vec = sample_times .into_iter() @@ -172,32 +184,29 @@ pub trait Curve { } times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); let samples = times.iter().copied().map(|t| self.sample(t)).collect(); - Ok(UnevenSampleAutoCurve { times, samples }) + Ok(UnevenSampleCurve { + core: UnevenSampleCore { times, samples }, + interpolation, + }) } /// 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. + /// 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 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( + /// 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, - interpolation: I, - ) -> Result, ResamplingError> + ) -> Result, ResamplingError> where Self: Sized, - I: Fn(&T, &T, f32) -> T, + T: Interpolable, { let mut times: Vec = sample_times .into_iter() @@ -209,10 +218,8 @@ pub trait Curve { } times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); let samples = times.iter().copied().map(|t| self.sample(t)).collect(); - Ok(UnevenSampleCurve { - times, - samples, - interpolation, + Ok(UnevenSampleAutoCurve { + core: UnevenSampleCore { times, samples }, }) } @@ -418,63 +425,10 @@ where } } -/// 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))] -pub struct SampleAutoCurve -where - T: Interpolable, -{ - domain: Interval, - /// The samples that make up this [`SampleCurve`] by interpolation. - /// - /// Invariant: this must always have a length of at least 2. - samples: Vec, -} - -impl Curve for SampleAutoCurve -where - T: Interpolable, -{ - #[inline] - fn domain(&self) -> Interval { - self.domain - } - - #[inline] - fn sample(&self, t: f32) -> T { - // Inside the curve itself, we interpolate between the two nearest sample values. - let subdivs = self.samples.len() - 1; - let step = self.domain.length() / subdivs as f32; - let t_shifted = t - self.domain.start(); - let steps_taken = t_shifted / step; - - // Using `steps_taken` as the source of truth, clamp to the range of valid indices. - if steps_taken <= 0.0 { - self.samples.first().unwrap().clone() - } else if steps_taken >= (self.samples.len() - 1) as f32 { - self.samples.last().unwrap().clone() - } else { - // Here we use only the floor and the fractional part of `steps_taken` to interpolate - // between the two nearby sample points. - let lower_index = steps_taken.floor() as usize; - - // Explicitly clamp the lower index just in case. - let lower_index = lower_index.min(self.samples.len() - 2); - let upper_index = lower_index + 1; - let fract = steps_taken.fract(); - self.samples[lower_index].interpolate(&self.samples[upper_index], fract) - } - } -} - /// A [`Curve`] that is defined by explicit neighbor interpolation over a set of samples. +#[derive(Clone, Debug)] pub struct SampleCurve { - domain: Interval, - /// The samples that make up this curve by interpolation. - /// - /// Invariant: this must always have a length of at least 2. - samples: Vec, + core: SampleCore, interpolation: I, } @@ -485,37 +439,12 @@ where { #[inline] fn domain(&self) -> Interval { - self.domain + self.core.domain() } #[inline] fn sample(&self, t: f32) -> T { - // Inside the curve itself, we interpolate between the two nearest sample values. - let subdivs = self.samples.len() - 1; - let step = self.domain.length() / subdivs as f32; - let t_shifted = t - self.domain.start(); - let steps_taken = t_shifted / step; - - // Using `steps_taken` as the source of truth, clamp to the range of valid indices. - if steps_taken <= 0.0 { - self.samples.first().unwrap().clone() - } else if steps_taken >= (self.samples.len() - 1) as f32 { - self.samples.last().unwrap().clone() - } else { - // Here we use only the floor and the fractional part of `steps_taken` to interpolate - // between the two nearby sample points. - let lower_index = steps_taken.floor() as usize; - - // Explicitly clamp the lower index just in case. - let lower_index = lower_index.min(self.samples.len() - 2); - let upper_index = lower_index + 1; - let fract = steps_taken.fract(); - (self.interpolation)( - &self.samples[lower_index], - &self.samples[upper_index], - fract, - ) - } + self.core.sample_with(t, &self.interpolation) } } @@ -531,115 +460,55 @@ impl SampleCurve { domain: Interval, samples: impl Into>, interpolation: I, - ) -> Result + ) -> Result where I: Fn(&T, &T, f32) -> T, { - let samples: Vec = samples.into(); - if samples.len() < 2 { - return Err(ResamplingError::NotEnoughSamples(samples.len())); - } - if !domain.is_finite() { - return Err(ResamplingError::InfiniteInterval(InfiniteIntervalError)); - } - - Ok(SampleCurve { - domain, - samples, + Ok(Self { + core: SampleCore::new(domain, samples)?, interpolation, }) } } -/// A [`Curve`] that is defined by interpolation over unevenly spaced samples. +/// 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))] -pub struct UnevenSampleAutoCurve -where - T: Interpolable, -{ - /// 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. - times: Vec, - - /// The samples corresponding to the times for this curve. - /// - /// Invariants: This must always have the same length as `times`. - samples: Vec, -} - -impl UnevenSampleAutoCurve -where - T: Interpolable, -{ - /// 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleAutoCurve { - 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 - } +pub struct SampleAutoCurve { + core: SampleCore, } -impl Curve for UnevenSampleAutoCurve +impl Curve for SampleAutoCurve where T: Interpolable, { #[inline] fn domain(&self) -> Interval { - let start = self.times.first().unwrap(); - let end = self.times.last().unwrap(); - Interval::new(*start, *end).unwrap() + self.core.domain() } #[inline] fn sample(&self, t: f32) -> T { - match self - .times - .binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) - { - Ok(index) => self.samples[index].clone(), - Err(index) => { - if index == 0 { - self.samples.first().unwrap().clone() - } else if index == self.times.len() { - self.samples.last().unwrap().clone() - } else { - let t_lower = self.times[index - 1]; - let v_lower = self.samples.get(index - 1).unwrap(); - let t_upper = self.times[index]; - let v_upper = self.samples.get(index).unwrap(); - let s = (t - t_lower) / (t_upper - t_lower); - v_lower.interpolate(v_upper, s) - } - } - } + self.core.sample_with(t, ::interpolate) + } +} + +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: SampleCore::new(domain, samples)?, + }) } } /// A [`Curve`] that is defined by interpolation over unevenly spaced samples with explicit /// interpolation. +#[derive(Clone, Debug)] pub struct UnevenSampleCurve { - /// 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. - times: Vec, - - /// The samples corresponding to the times for this curve. - /// - /// Invariants: This must always have the same length as `times`. - samples: Vec, + core: UnevenSampleCore, interpolation: I, } @@ -650,33 +519,12 @@ where { #[inline] fn domain(&self) -> Interval { - let start = self.times.first().unwrap(); - let end = self.times.last().unwrap(); - Interval::new(*start, *end).unwrap() + self.core.domain() } #[inline] fn sample(&self, t: f32) -> T { - match self - .times - .binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) - { - Ok(index) => self.samples[index].clone(), - Err(index) => { - if index == 0 { - self.samples.first().unwrap().clone() - } else if index == self.times.len() { - self.samples.last().unwrap().clone() - } else { - let t_lower = self.times[index - 1]; - let v_lower = self.samples.get(index - 1).unwrap(); - let t_upper = self.times[index]; - let v_upper = self.samples.get(index).unwrap(); - let s = (t - t_lower) / (t_upper - t_lower); - (self.interpolation)(v_lower, v_upper, s) - } - } - } + self.core.sample_with(t, &self.interpolation) } } @@ -692,22 +540,9 @@ impl UnevenSampleCurve { pub fn new( timed_samples: impl Into>, interpolation: I, - ) -> Result { - let mut timed_samples: Vec<(f32, T)> = timed_samples.into(); - // Use default Equal to not do anything in case NAN appears; it will get removed in the - // next step anyway. - timed_samples - .sort_by(|(t0, _), (t1, _)| t0.partial_cmp(t1).unwrap_or(std::cmp::Ordering::Equal)); - let (times, samples): (Vec, Vec) = timed_samples - .into_iter() - .filter(|(t, _)| t.is_finite()) - .unzip(); - if times.len() < 2 { - return Err(ResamplingError::NotEnoughSamples(times.len())); - } - Ok(UnevenSampleCurve { - times, - samples, + ) -> Result { + Ok(Self { + core: UnevenSampleCore::new(timed_samples)?, interpolation, }) } @@ -718,14 +553,57 @@ impl UnevenSampleCurve { /// /// 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(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { - 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 + 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))] +pub struct UnevenSampleAutoCurve { + core: UnevenSampleCore, +} + +impl Curve for UnevenSampleAutoCurve +where + T: Interpolable, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample(&self, t: f32) -> T { + self.core.sample_with(t, ::interpolate) + } +} + +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: UnevenSampleCore::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), + } } } From d8e45daa5541dab2c37e93ac2b30ec8c70001b1f Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 10 Jun 2024 16:19:43 -0400 Subject: [PATCH 18/28] Rename builders -> cores, refactoring to Betweenness --- crates/bevy_math/src/curve/builders.rs | 285 -------------- crates/bevy_math/src/curve/cores.rs | 503 +++++++++++++++++++++++++ crates/bevy_math/src/curve/mod.rs | 36 +- 3 files changed, 521 insertions(+), 303 deletions(-) delete mode 100644 crates/bevy_math/src/curve/builders.rs create mode 100644 crates/bevy_math/src/curve/cores.rs diff --git a/crates/bevy_math/src/curve/builders.rs b/crates/bevy_math/src/curve/builders.rs deleted file mode 100644 index 9af37df0ce43f..0000000000000 --- a/crates/bevy_math/src/curve/builders.rs +++ /dev/null @@ -1,285 +0,0 @@ -//! Core data structures to be used internally in Curve implementations. - -use super::interval::Interval; -use thiserror::Error; - -/// 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]: SampleCore::new -/// [`domain`]: SampleCore::domain -/// [`sample_with`]: SampleCore::sample_with -/// -/// # Example -/// ```rust -/// # use bevy_math::curve::*; -/// # use bevy_math::curve::builders::*; -/// 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: SampleCore, -/// 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)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct SampleCore { - /// 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 [`SampleCore`] could not be constructed. -#[derive(Debug, Error)] -pub enum SampleCoreError { - /// Not enough samples were provided. - #[error("Need at least two samples to create a SampleCore, but {samples} were provided")] - NotEnoughSamples { - /// The number of samples that were provided. - samples: usize, - }, - - /// Unbounded domains are not compatible with `SampleCore`. - #[error("Cannot create a SampleCore over a domain with an infinite endpoint")] - InfiniteDomain, -} - -impl SampleCore { - /// Create a new [`SampleCore`] 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(SampleCoreError::NotEnoughSamples { - samples: samples.len(), - }); - } - if !domain.is_finite() { - return Err(SampleCoreError::InfiniteDomain); - } - - Ok(SampleCore { 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, - { - // Inside the curve itself, we interpolate between the two nearest sample values. - let subdivs = self.samples.len() - 1; - let step = self.domain.length() / subdivs as f32; - let t_shifted = t - self.domain.start(); - let steps_taken = t_shifted / step; - - // Using `steps_taken` as the source of truth, clamp to the range of valid indices. - if steps_taken <= 0.0 { - self.samples.first().unwrap().clone() - } else if steps_taken >= (self.samples.len() - 1) as f32 { - self.samples.last().unwrap().clone() - } else { - // Here we use only the floor and the fractional part of `steps_taken` to interpolate - // between the two nearby sample points. - let lower_index = steps_taken.floor() as usize; - - // Explicitly clamp the lower index just in case. - let lower_index = lower_index.min(self.samples.len() - 2); - let upper_index = lower_index + 1; - let fract = steps_taken.fract(); - interpolation( - &self.samples[lower_index], - &self.samples[upper_index], - fract, - ) - } - } -} - -/// 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`]. -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct UnevenSampleCore { - /// 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 [`UnevenSampleCore`] could not be constructed. -#[derive(Debug, Error)] -pub enum UnevenSampleCoreError { - /// Not enough samples were provided. - #[error( - "Need at least two samples to create an UnevenSampleCore, but {samples} were provided" - )] - NotEnoughSamples { - /// The number of samples that were provided. - samples: usize, - }, -} - -impl UnevenSampleCore { - /// Create a new [`UnevenSampleCore`] 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>) -> Result { - let mut timed_samples: Vec<(f32, T)> = timed_samples.into(); - // Use default Equal to not do anything in case NAN appears; it will get removed in the - // next step anyway. - timed_samples - .sort_by(|(t0, _), (t1, _)| t0.partial_cmp(t1).unwrap_or(std::cmp::Ordering::Equal)); - let (times, samples): (Vec, Vec) = timed_samples - .into_iter() - .filter(|(t, _)| t.is_finite()) - .unzip(); - if times.len() < 2 { - return Err(UnevenSampleCoreError::NotEnoughSamples { - samples: times.len(), - }); - } - Ok(UnevenSampleCore { 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 self - .times - .binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) - { - Ok(index) => self.samples[index].clone(), - Err(index) => { - if index == 0 { - self.samples.first().unwrap().clone() - } else if index == self.times.len() { - self.samples.last().unwrap().clone() - } else { - let t_lower = self.times[index - 1]; - let v_lower = self.samples.get(index - 1).unwrap(); - let t_upper = self.times[index]; - let v_upper = self.samples.get(index).unwrap(); - let s = (t - t_lower) / (t_upper - t_lower); - interpolation(v_lower, v_upper, s) - } - } - } - } - - /// 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. - pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenSampleCore { - 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 taken more than one at a time. -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct ChunkedUnevenSampleCore { - times: Vec, - samples_serial: Vec, - chunk_width: usize, -} diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs new file mode 100644 index 0000000000000..57a13cdeee49b --- /dev/null +++ b/crates/bevy_math/src/curve/cores.rs @@ -0,0 +1,503 @@ +//! Core data structures to be used internally in Curve implementations, encapsulating storage +//! and access patterns for reuse. + +use super::interval::Interval; +use thiserror::Error; + +/// This type expresses the relationship of a value to a linear collection of values. It is a kind +/// of summary used intermediately by sampling operations. +pub enum Betweenness { + /// This value lies exactly on another. + 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 Betweenness { + /// Map all values using a given function `f`, leaving the interpolation parameters in any + /// [`Between`] variants unchanged. + /// + /// [`Between`]: `Betweenness::Between` + #[must_use] + pub fn map(self, f: impl Fn(T) -> S) -> Betweenness { + match self { + Betweenness::Exact(v) => Betweenness::Exact(f(v)), + Betweenness::LeftTail(v) => Betweenness::LeftTail(f(v)), + Betweenness::RightTail(v) => Betweenness::RightTail(f(v)), + Betweenness::Between(u, v, s) => Betweenness::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::builders::*; +/// 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)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +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)] +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_betweenness(self.domain, self.samples.len(), t) { + Betweenness::Exact(idx) | Betweenness::LeftTail(idx) | Betweenness::RightTail(idx) => { + self.samples[idx].clone() + } + Betweenness::Between(lower_idx, upper_idx, s) => { + interpolation(&self.samples[lower_idx], &self.samples[upper_idx], s) + } + } + } + + /// Given a time `t`, obtain a [`Betweenness`] 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`]: `Betweenness::Between` + pub fn sample_betweenness(&self, t: f32) -> Betweenness<&T> { + even_betweenness(self.domain, self.samples.len(), t).map(|idx| &self.samples[idx]) + } +} + +/// Given a domain and a number of samples taken over that interval, return a [`Betweenness`] +/// 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_betweenness(domain: Interval, samples: usize, t: f32) -> Betweenness { + 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. + Betweenness::LeftTail(0) + } else if steps_taken >= subdivs as f32 { + // To the right side of all the samples + Betweenness::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(); + Betweenness::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`]. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +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)] +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_betweenness(&self.times, t) { + Betweenness::Exact(idx) | Betweenness::LeftTail(idx) | Betweenness::RightTail(idx) => { + self.samples[idx].clone() + } + Betweenness::Between(lower_idx, upper_idx, s) => { + interpolation(&self.samples[lower_idx], &self.samples[upper_idx], s) + } + } + } + + /// Given a time `t`, obtain a [`Betweenness`] 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`]: `Betweenness::Between` + pub fn sample_betweenness(&self, t: f32) -> Betweenness<&T> { + uneven_betweenness(&self.times, t).map(|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. + 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 array. +/// +/// [sampling width]: ChunkedUnevenCore::width +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +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` of these correspond to a single sample. + /// + /// # Invariants + /// This must always have a length of `width` times that of `times`. + pub values: Vec, + + /// The sampling width, determining how many consecutive elements of `values` are taken in a + /// single sample. + /// + /// # Invariants + /// This must never be zero. + pub width: usize, +} + +/// An error that indicates that a [`ChunkedUnevenCore`] could not be formed. +#[derive(Debug, Error)] +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, + width, + }) + } + + /// 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() + } + + /// Given a time `t`, obtain a [`Betweenness`] 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`]: `Betweenness::Between` + #[inline] + pub fn sample_betweenness(&self, t: f32) -> Betweenness<&[T]> { + uneven_betweenness(&self.times, t).map(|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 lower_idx = self.width * idx; + let upper_idx = lower_idx + self.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 betweenness 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_betweenness(times: &[f32], t: f32) -> Betweenness { + match times.binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) { + Ok(index) => Betweenness::Exact(index), + Err(index) => { + if index == 0 { + // This is before the first keyframe. + Betweenness::LeftTail(0) + } else if index >= times.len() { + // This is after the last keyframe. + Betweenness::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); + Betweenness::Between(index - 1, index, s) + } + } + } +} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 4d8df0d4611b9..d273e77ae81f1 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -1,14 +1,14 @@ //! The [`Curve`] trait, used to describe curves in a number of different domains. This module also //! contains the [`Interpolable`] trait and the [`Interval`] type. -pub mod builders; +pub mod cores; pub mod interpolable; pub mod interval; pub use interpolable::Interpolable; pub use interval::{everywhere, interval, Interval}; -use builders::{SampleCore, SampleCoreError, UnevenSampleCore, UnevenSampleCoreError}; +use cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError}; use interval::{InfiniteIntervalError, InvalidIntervalError}; use std::{marker::PhantomData, ops::Deref}; use thiserror::Error; @@ -94,7 +94,7 @@ pub trait Curve { .map(|t| self.sample(t)) .collect(); Ok(SampleCurve { - core: SampleCore { + core: EvenCore { domain: self.domain(), samples, }, @@ -124,7 +124,7 @@ pub trait Curve { .map(|t| self.sample(t)) .collect(); Ok(SampleAutoCurve { - core: SampleCore { + core: EvenCore { domain: self.domain(), samples, }, @@ -185,7 +185,7 @@ pub trait Curve { times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); let samples = times.iter().copied().map(|t| self.sample(t)).collect(); Ok(UnevenSampleCurve { - core: UnevenSampleCore { times, samples }, + core: UnevenCore { times, samples }, interpolation, }) } @@ -219,7 +219,7 @@ pub trait Curve { times.sort_by(|t1, t2| t1.partial_cmp(t2).unwrap()); let samples = times.iter().copied().map(|t| self.sample(t)).collect(); Ok(UnevenSampleAutoCurve { - core: UnevenSampleCore { times, samples }, + core: UnevenCore { times, samples }, }) } @@ -428,7 +428,7 @@ where /// A [`Curve`] that is defined by explicit neighbor interpolation over a set of samples. #[derive(Clone, Debug)] pub struct SampleCurve { - core: SampleCore, + core: EvenCore, interpolation: I, } @@ -460,12 +460,12 @@ impl SampleCurve { domain: Interval, samples: impl Into>, interpolation: I, - ) -> Result + ) -> Result where I: Fn(&T, &T, f32) -> T, { Ok(Self { - core: SampleCore::new(domain, samples)?, + core: EvenCore::new(domain, samples)?, interpolation, }) } @@ -475,7 +475,7 @@ impl SampleCurve { #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct SampleAutoCurve { - core: SampleCore, + core: EvenCore, } impl Curve for SampleAutoCurve @@ -497,9 +497,9 @@ 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 Into>) -> Result { Ok(Self { - core: SampleCore::new(domain, samples)?, + core: EvenCore::new(domain, samples)?, }) } } @@ -508,7 +508,7 @@ impl SampleAutoCurve { /// interpolation. #[derive(Clone, Debug)] pub struct UnevenSampleCurve { - core: UnevenSampleCore, + core: UnevenCore, interpolation: I, } @@ -540,9 +540,9 @@ impl UnevenSampleCurve { pub fn new( timed_samples: impl Into>, interpolation: I, - ) -> Result { + ) -> Result { Ok(Self { - core: UnevenSampleCore::new(timed_samples)?, + core: UnevenCore::new(timed_samples)?, interpolation, }) } @@ -565,7 +565,7 @@ impl UnevenSampleCurve { #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct UnevenSampleAutoCurve { - core: UnevenSampleCore, + core: UnevenCore, } impl Curve for UnevenSampleAutoCurve @@ -588,9 +588,9 @@ 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 Into>) -> Result { Ok(Self { - core: UnevenSampleCore::new(timed_samples)?, + core: UnevenCore::new(timed_samples)?, }) } From 52f14c1a3d547ec335698255628938298490bbbb Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 10 Jun 2024 16:31:21 -0400 Subject: [PATCH 19/28] Kill Interpolable and transition to StableInterpolate --- crates/bevy_math/src/curve/interpolable.rs | 39 ---------------------- crates/bevy_math/src/curve/mod.rs | 20 ++++++----- 2 files changed, 11 insertions(+), 48 deletions(-) delete mode 100644 crates/bevy_math/src/curve/interpolable.rs diff --git a/crates/bevy_math/src/curve/interpolable.rs b/crates/bevy_math/src/curve/interpolable.rs deleted file mode 100644 index 9458e1528c8e6..0000000000000 --- a/crates/bevy_math/src/curve/interpolable.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! The [`Interpolable`] trait for types that support interpolation between two values. - -use crate::{Quat, VectorSpace}; - -/// A trait for types whose values can be intermediately interpolated between two given values -/// with an auxiliary parameter. -pub trait Interpolable: Clone { - /// Interpolate between this value and the `other` given value using the parameter `t`. - /// Note that the parameter `t` is not necessarily clamped to lie between `0` and `1`. - fn interpolate(&self, other: &Self, t: f32) -> Self; -} - -impl Interpolable for (S, T) -where - S: Interpolable, - T: Interpolable, -{ - fn interpolate(&self, other: &Self, t: f32) -> Self { - ( - self.0.interpolate(&other.0, t), - self.1.interpolate(&other.1, t), - ) - } -} - -impl Interpolable for T -where - T: VectorSpace, -{ - fn interpolate(&self, other: &Self, t: f32) -> Self { - self.lerp(*other, t) - } -} - -impl Interpolable for Quat { - fn interpolate(&self, other: &Self, t: f32) -> Self { - self.slerp(*other, t) - } -} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index d273e77ae81f1..ac070254f1170 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -1,13 +1,13 @@ //! The [`Curve`] trait, used to describe curves in a number of different domains. This module also -//! contains the [`Interpolable`] trait and the [`Interval`] type. +//! 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 interpolable; pub mod interval; -pub use interpolable::Interpolable; 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}; @@ -108,7 +108,7 @@ pub trait Curve { /// or if this curve has an unbounded domain, then a [`ResamplingError`] is returned. fn resample_auto(&self, samples: usize) -> Result, ResamplingError> where - T: Interpolable, + T: StableInterpolate, { if samples < 2 { return Err(ResamplingError::NotEnoughSamples(samples)); @@ -206,7 +206,7 @@ pub trait Curve { ) -> Result, ResamplingError> where Self: Sized, - T: Interpolable, + T: StableInterpolate, { let mut times: Vec = sample_times .into_iter() @@ -480,7 +480,7 @@ pub struct SampleAutoCurve { impl Curve for SampleAutoCurve where - T: Interpolable, + T: StableInterpolate, { #[inline] fn domain(&self) -> Interval { @@ -489,7 +489,8 @@ where #[inline] fn sample(&self, t: f32) -> T { - self.core.sample_with(t, ::interpolate) + self.core + .sample_with(t, ::interpolate_stable) } } @@ -570,7 +571,7 @@ pub struct UnevenSampleAutoCurve { impl Curve for UnevenSampleAutoCurve where - T: Interpolable, + T: StableInterpolate, { #[inline] fn domain(&self) -> Interval { @@ -579,7 +580,8 @@ where #[inline] fn sample(&self, t: f32) -> T { - self.core.sample_with(t, ::interpolate) + self.core + .sample_with(t, ::interpolate_stable) } } From 8113c7fb8aa541d40ce7627a969a42e6e2c7ac1d Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 10 Jun 2024 17:36:29 -0400 Subject: [PATCH 20/28] Derive Reflect on many things --- crates/bevy_math/src/curve/cores.rs | 22 ++++++++++-- crates/bevy_math/src/curve/interval.rs | 10 ++++++ crates/bevy_math/src/curve/mod.rs | 48 ++++++++++++++++++-------- 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 57a13cdeee49b..829321e31a338 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -2,12 +2,19 @@ //! and access patterns for reuse. 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 linear 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 Betweenness { - /// This value lies exactly on another. + /// 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. @@ -90,8 +97,9 @@ impl Betweenness { /// } /// } /// ``` -#[derive(Debug, Clone)] +#[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. @@ -108,7 +116,9 @@ pub struct EvenCore { } /// An error indicating that a [`EvenCore`] could not be constructed. -#[derive(Debug, Error)] +#[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")] @@ -214,6 +224,7 @@ pub fn even_betweenness(domain: Interval, samples: usize, t: f32) -> Betweenness /// order to implement the curve interface using [`domain`] and [`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. /// @@ -231,6 +242,8 @@ pub struct UnevenCore { /// 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")] @@ -338,6 +351,7 @@ impl UnevenCore { /// [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. /// @@ -362,6 +376,8 @@ pub struct ChunkedUnevenCore { /// 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")] diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index 0a1d8b2739c94..834ded8443007 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -6,9 +6,19 @@ use std::{ }; 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, diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index ac070254f1170..99cd2914ac412 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -13,19 +13,8 @@ use interval::{InfiniteIntervalError, InvalidIntervalError}; use std::{marker::PhantomData, ops::Deref}; use thiserror::Error; -/// 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), -} +#[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 @@ -67,7 +56,7 @@ pub trait Curve { /// ``` /// # use bevy_math::*; /// # use bevy_math::curve::*; - /// let quarter_rotation = function_curve(interval(0.0, 90.0).unwrap(), |t| Rotation2d::degrees(t)); + /// 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)); /// ``` @@ -374,9 +363,24 @@ 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), +} + /// 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 where T: Clone, @@ -402,6 +406,8 @@ where /// 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 where F: Fn(f32) -> T, @@ -427,6 +433,8 @@ 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, @@ -474,6 +482,7 @@ impl SampleCurve { /// 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, } @@ -508,6 +517,8 @@ impl SampleAutoCurve { /// 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, @@ -565,6 +576,7 @@ impl UnevenSampleCurve { /// 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, } @@ -612,6 +624,8 @@ impl UnevenSampleAutoCurve { /// 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 where C: Curve, @@ -667,6 +681,8 @@ where /// 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 where C: Curve, @@ -728,6 +744,8 @@ where /// Briefly, the point is that the curve just absorbs new functions instead of rebasing /// itself inside new structs. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct MapReparamCurve where C: Curve, @@ -791,6 +809,7 @@ where /// 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 where C: Curve, @@ -817,6 +836,7 @@ where /// 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 where C: Curve, From 0e58d03b9457e20b1fcc60c98c7da60c73dc4df2 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 10 Jun 2024 17:53:05 -0400 Subject: [PATCH 21/28] Fix docs --- crates/bevy_math/src/curve/cores.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 829321e31a338..916c26c000e63 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -58,7 +58,7 @@ impl Betweenness { /// # Example /// ```rust /// # use bevy_math::curve::*; -/// # use bevy_math::curve::builders::*; +/// # use bevy_math::curve::cores::*; /// enum InterpolationMode { /// Linear, /// Step, @@ -222,6 +222,9 @@ pub fn even_betweenness(domain: Interval, samples: usize, t: f32) -> Betweenness /// 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))] @@ -332,6 +335,8 @@ impl UnevenCore { /// /// 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(); From 7beecd88f00c4068e7d72381a1a5b62cd63c8f2e Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 11 Jun 2024 06:54:37 -0400 Subject: [PATCH 22/28] Add timed versions of betweenness sampling --- crates/bevy_math/src/curve/cores.rs | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 916c26c000e63..aa9d59171d4dd 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -187,6 +187,20 @@ impl EvenCore { pub fn sample_betweenness(&self, t: f32) -> Betweenness<&T> { even_betweenness(self.domain, self.samples.len(), t).map(|idx| &self.samples[idx]) } + + /// Like [`sample_betweenness`], but the returned values include the sample times. This can be + /// useful when sampling is not scale-invariant. + /// + /// [`sample_betweenness`]: EvenCore::sample_betweenness + pub fn sample_betweenness_timed(&self, t: f32) -> Betweenness<(f32, &T)> { + let segment_len = self.domain.length() / (self.samples.len() - 1) as f32; + even_betweenness(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 [`Betweenness`] @@ -329,6 +343,14 @@ impl UnevenCore { uneven_betweenness(&self.times, t).map(|idx| &self.samples[idx]) } + /// Like [`sample_betweenness`], but the returned values include the sample times. This can be + /// useful when sampling is not scale-invariant. + /// + /// [`sample_betweenness`]: UnevenCore::sample_betweenness + pub fn sample_betweenness_timed(&self, t: f32) -> Betweenness<(f32, &T)> { + uneven_betweenness(&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. @@ -471,6 +493,15 @@ impl ChunkedUnevenCore { uneven_betweenness(&self.times, t).map(|idx| self.time_index_to_slice(idx)) } + /// Like [`sample_betweenness`], but the returned values include the sample times. This can be + /// useful when sampling is not scale-invariant. + /// + /// [`sample_betweenness`]: ChunkedUnevenCore::sample_betweenness + pub fn sample_betweenness_timed(&self, t: f32) -> Betweenness<(f32, &[T])> { + uneven_betweenness(&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. /// From cb1a6963c3e1264835f9347cf1f97b6806e09b99 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 11 Jun 2024 21:55:50 -0400 Subject: [PATCH 23/28] Reduce size of ChunkedUnevenCore --- crates/bevy_math/src/curve/cores.rs | 34 +++++++++++++---------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index aa9d59171d4dd..955a16fc346d6 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -373,7 +373,7 @@ impl UnevenCore { /// 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 array. +/// if the sample type can effectively be encoded as a fixed-length slice of values. /// /// [sampling width]: ChunkedUnevenCore::width #[derive(Debug, Clone)] @@ -383,22 +383,15 @@ 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. + /// 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` of these correspond to a single sample. + /// The values that are used in sampling. Each width-worth of these correspond to a single sample. /// /// # Invariants - /// This must always have a length of `width` times that of `times`. + /// The length of this vector must always be some fixed integer multiple of that of `times`. pub values: Vec, - - /// The sampling width, determining how many consecutive elements of `values` are taken in a - /// single sample. - /// - /// # Invariants - /// This must never be zero. - pub width: usize, } /// An error that indicates that a [`ChunkedUnevenCore`] could not be formed. @@ -464,11 +457,7 @@ impl ChunkedUnevenCore { }); } - Ok(Self { - times, - values, - width, - }) + Ok(Self { times, values }) } /// The domain of the curve derived from this core. @@ -482,6 +471,12 @@ impl ChunkedUnevenCore { 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 [`Betweenness`] 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 @@ -509,8 +504,9 @@ impl ChunkedUnevenCore { /// [values]: ChunkedUnevenCore::values #[inline] fn time_index_to_slice(&self, idx: usize) -> &[T] { - let lower_idx = self.width * idx; - let upper_idx = lower_idx + self.width; + let width = self.width(); + let lower_idx = width * idx; + let upper_idx = lower_idx + width; &self.values[lower_idx..upper_idx] } } From cc8970a5313eb5ab5d05041b9aaaf3e1ef3eada1 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Fri, 14 Jun 2024 17:49:30 -0400 Subject: [PATCH 24/28] Rename Betweenness -> InterpolationDatum --- crates/bevy_math/src/curve/cores.rs | 78 ++++++++++++++--------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 955a16fc346d6..2e3e97b245d93 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -8,12 +8,12 @@ use thiserror::Error; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; -/// This type expresses the relationship of a value to a linear collection of values. It is a kind +/// 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 Betweenness { +pub enum InterpolationDatum { /// This value lies exactly on a value in the family. Exact(T), @@ -28,18 +28,18 @@ pub enum Betweenness { Between(T, T, f32), } -impl Betweenness { +impl InterpolationDatum { /// Map all values using a given function `f`, leaving the interpolation parameters in any /// [`Between`] variants unchanged. /// - /// [`Between`]: `Betweenness::Between` + /// [`Between`]: `InterpolationDatum::Between` #[must_use] - pub fn map(self, f: impl Fn(T) -> S) -> Betweenness { + pub fn map(self, f: impl Fn(T) -> S) -> InterpolationDatum { match self { - Betweenness::Exact(v) => Betweenness::Exact(f(v)), - Betweenness::LeftTail(v) => Betweenness::LeftTail(f(v)), - Betweenness::RightTail(v) => Betweenness::RightTail(f(v)), - Betweenness::Between(u, v, s) => Betweenness::Between(f(u), f(v), s), + 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), } } } @@ -169,22 +169,22 @@ impl EvenCore { I: Fn(&T, &T, f32) -> T, { match even_betweenness(self.domain, self.samples.len(), t) { - Betweenness::Exact(idx) | Betweenness::LeftTail(idx) | Betweenness::RightTail(idx) => { - self.samples[idx].clone() - } - Betweenness::Between(lower_idx, upper_idx, s) => { + 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 [`Betweenness`] which governs how interpolation might recover + /// 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`]: `Betweenness::Between` - pub fn sample_betweenness(&self, t: f32) -> Betweenness<&T> { + /// [`Between`]: `InterpolationDatum::Between` + pub fn sample_betweenness(&self, t: f32) -> InterpolationDatum<&T> { even_betweenness(self.domain, self.samples.len(), t).map(|idx| &self.samples[idx]) } @@ -192,7 +192,7 @@ impl EvenCore { /// useful when sampling is not scale-invariant. /// /// [`sample_betweenness`]: EvenCore::sample_betweenness - pub fn sample_betweenness_timed(&self, t: f32) -> Betweenness<(f32, &T)> { + pub fn sample_betweenness_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> { let segment_len = self.domain.length() / (self.samples.len() - 1) as f32; even_betweenness(self.domain, self.samples.len(), t).map(|idx| { ( @@ -203,7 +203,7 @@ impl EvenCore { } } -/// Given a domain and a number of samples taken over that interval, return a [`Betweenness`] +/// 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`). @@ -211,7 +211,7 @@ impl EvenCore { /// `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_betweenness(domain: Interval, samples: usize, t: f32) -> Betweenness { +pub fn even_betweenness(domain: Interval, samples: usize, t: f32) -> InterpolationDatum { let subdivs = samples - 1; let step = domain.length() / subdivs as f32; let t_shifted = t - domain.start(); @@ -219,17 +219,17 @@ pub fn even_betweenness(domain: Interval, samples: usize, t: f32) -> Betweenness if steps_taken <= 0.0 { // To the left side of all the samples. - Betweenness::LeftTail(0) + InterpolationDatum::LeftTail(0) } else if steps_taken >= subdivs as f32 { // To the right side of all the samples - Betweenness::RightTail(samples - 1) + 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(); - Betweenness::Between(lower_index, upper_index, s) + InterpolationDatum::Between(lower_index, upper_index, s) } } @@ -324,22 +324,22 @@ impl UnevenCore { I: Fn(&T, &T, f32) -> T, { match uneven_betweenness(&self.times, t) { - Betweenness::Exact(idx) | Betweenness::LeftTail(idx) | Betweenness::RightTail(idx) => { - self.samples[idx].clone() - } - Betweenness::Between(lower_idx, upper_idx, s) => { + 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 [`Betweenness`] which governs how interpolation might recover + /// 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`]: `Betweenness::Between` - pub fn sample_betweenness(&self, t: f32) -> Betweenness<&T> { + /// [`Between`]: `InterpolationDatum::Between` + pub fn sample_betweenness(&self, t: f32) -> InterpolationDatum<&T> { uneven_betweenness(&self.times, t).map(|idx| &self.samples[idx]) } @@ -347,7 +347,7 @@ impl UnevenCore { /// useful when sampling is not scale-invariant. /// /// [`sample_betweenness`]: UnevenCore::sample_betweenness - pub fn sample_betweenness_timed(&self, t: f32) -> Betweenness<(f32, &T)> { + pub fn sample_betweenness_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> { uneven_betweenness(&self.times, t).map(|idx| (self.times[idx], &self.samples[idx])) } @@ -477,14 +477,14 @@ impl ChunkedUnevenCore { self.values.len() / self.times.len() } - /// Given a time `t`, obtain a [`Betweenness`] which governs how interpolation might recover + /// 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`]: `Betweenness::Between` + /// [`Between`]: `InterpolationDatum::Between` #[inline] - pub fn sample_betweenness(&self, t: f32) -> Betweenness<&[T]> { + pub fn sample_betweenness(&self, t: f32) -> InterpolationDatum<&[T]> { uneven_betweenness(&self.times, t).map(|idx| self.time_index_to_slice(idx)) } @@ -492,7 +492,7 @@ impl ChunkedUnevenCore { /// useful when sampling is not scale-invariant. /// /// [`sample_betweenness`]: ChunkedUnevenCore::sample_betweenness - pub fn sample_betweenness_timed(&self, t: f32) -> Betweenness<(f32, &[T])> { + pub fn sample_betweenness_timed(&self, t: f32) -> InterpolationDatum<(f32, &[T])> { uneven_betweenness(&self.times, t) .map(|idx| (self.times[idx], self.time_index_to_slice(idx))) } @@ -529,22 +529,22 @@ fn filter_sort_dedup_times(times: Vec) -> Vec { /// /// # Panics /// This function will panic if `times` contains NAN. -pub fn uneven_betweenness(times: &[f32], t: f32) -> Betweenness { +pub fn uneven_betweenness(times: &[f32], t: f32) -> InterpolationDatum { match times.binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) { - Ok(index) => Betweenness::Exact(index), + Ok(index) => InterpolationDatum::Exact(index), Err(index) => { if index == 0 { // This is before the first keyframe. - Betweenness::LeftTail(0) + InterpolationDatum::LeftTail(0) } else if index >= times.len() { // This is after the last keyframe. - Betweenness::RightTail(times.len() - 1) + 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); - Betweenness::Between(index - 1, index, s) + InterpolationDatum::Between(index - 1, index, s) } } } From 97bdaed8f1c6adbf1710727904c013106979b564 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Fri, 14 Jun 2024 19:14:30 -0400 Subject: [PATCH 25/28] Add end-to-end composition --- crates/bevy_math/src/curve/interval.rs | 14 ++++- crates/bevy_math/src/curve/mod.rs | 82 +++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index 834ded8443007..0edbd42aad94a 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -84,12 +84,24 @@ impl Interval { self.end - self.start } - /// Returns `true` if this interval is finite. + /// 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 { diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 99cd2914ac412..5ac1c008f45a3 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -308,7 +308,7 @@ pub trait Curve { } } - /// Create a new [`Curve`] by joining this curve together with another. The sample at time `t` + /// 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 @@ -327,6 +327,28 @@ 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 [`CompositionError`] 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 compose(self, other: C) -> Result, CompositionError> + where + Self: Sized, + C: Curve, + { + if !self.domain().is_right_finite() { + return Err(CompositionError::RightInfiniteFirst); + } + if !other.domain().is_left_finite() { + return Err(CompositionError::LeftInfiniteSecond); + } + Ok(ComposeCurve { + 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. @@ -377,6 +399,20 @@ pub enum ResamplingError { 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 CompositionError { + /// 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))] @@ -864,6 +900,50 @@ where } } +/// The [`Curve`] that results from composing 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 ComposeCurve +where + C: Curve, + D: Curve, +{ + first: C, + second: D, + _phantom: PhantomData, +} + +impl Curve for ComposeCurve +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) -> impl Curve { ConstantCurve { domain, value } From ff4f8fc8a27ead38f3f5331a169b37a02ec23fe4 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 17 Jun 2024 15:53:45 -0400 Subject: [PATCH 26/28] Scoured RPIT from the API --- crates/bevy_math/src/curve/mod.rs | 244 +++++++++++------------------- 1 file changed, 88 insertions(+), 156 deletions(-) diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 5ac1c008f45a3..1f6cdb7624085 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -215,9 +215,10 @@ 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)`. - fn map(self, f: impl Fn(T) -> S) -> impl Curve + fn map(self, f: F) -> MapCurve where Self: Sized, + F: Fn(T) -> S, { MapCurve { preimage: self, @@ -263,9 +264,10 @@ pub trait Curve { /// let domain = my_curve.domain(); /// let eased_curve = my_curve.reparametrize(domain, |t| easing_curve.sample(t).y); /// ``` - fn reparametrize(self, domain: Interval, f: impl Fn(f32) -> f32) -> impl Curve + fn reparametrize(self, domain: Interval, f: F) -> ReparamCurve where Self: Sized, + F: Fn(f32) -> f32, { ReparamCurve { domain, @@ -279,26 +281,44 @@ pub trait Curve { /// `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> + fn reparametrize_linear( + self, + domain: Interval, + ) -> Result, InfiniteIntervalError> where Self: Sized, { - let f = domain.linear_map_to(self.domain())?; - Ok(self.reparametrize(domain, f)) + if !domain.is_finite() { + return Err(InfiniteIntervalError); + } + + Ok(LinearReparamCurve { + base: self, + new_domain: domain, + _phantom: PhantomData, + }) } /// Reparametrize this [`Curve`] by sampling from another curve. - fn reparametrize_by_curve(self, other: &impl Curve) -> impl 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, { - self.reparametrize(other.domain(), |t| other.sample(t)) + 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) -> impl Curve<(f32, T)> + fn graph(self) -> GraphCurve where Self: Sized, { @@ -313,7 +333,7 @@ pub trait Curve { /// 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> + fn zip(self, other: C) -> Result, InvalidIntervalError> where Self: Sized, C: Curve + Sized, @@ -331,7 +351,7 @@ pub trait 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 [`CompositionError`] 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 compose(self, other: C) -> Result, CompositionError> + fn compose(self, other: C) -> Result, CompositionError> where Self: Sized, C: Curve, @@ -413,14 +433,11 @@ pub enum CompositionError { LeftInfiniteSecond, } -/// A [`Curve`] which takes a constant value over its domain. +/// 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 -where - T: Clone, -{ +pub struct ConstantCurve { domain: Interval, value: T, } @@ -440,16 +457,14 @@ where } } -/// A [`Curve`] defined by a function. +/// 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 -where - F: Fn(f32) -> T, -{ +pub struct FunctionCurve { domain: Interval, f: F, + _phantom: PhantomData, } impl Curve for FunctionCurve @@ -467,7 +482,7 @@ where } } -/// A [`Curve`] that is defined by explicit neighbor interpolation over a set of samples. +/// 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))] @@ -515,7 +530,7 @@ impl SampleCurve { } } -/// A [`Curve`] that is defined by neighbor interpolation over a set of samples. +/// 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))] @@ -550,7 +565,7 @@ impl SampleAutoCurve { } } -/// A [`Curve`] that is defined by interpolation over unevenly spaced samples with explicit +/// 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))] @@ -609,7 +624,7 @@ impl UnevenSampleCurve { } } -/// A [`Curve`] that is defined by interpolation over unevenly spaced samples. +/// 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))] @@ -657,16 +672,12 @@ impl UnevenSampleAutoCurve { } } -/// A [`Curve`] whose samples are defined by mapping samples from another curve through a +/// 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 -where - C: Curve, - F: Fn(S) -> T, -{ +pub struct MapCurve { preimage: C, f: F, _phantom: PhantomData<(S, T)>, @@ -686,44 +697,13 @@ where fn sample(&self, t: f32) -> T { (self.f)(self.preimage.sample(t)) } - - #[inline] - fn map(self, g: impl Fn(T) -> R) -> impl Curve - where - Self: Sized, - { - let gf = move |x| g((self.f)(x)); - MapCurve { - preimage: self.preimage, - f: gf, - _phantom: PhantomData, - } - } - - #[inline] - fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve - where - Self: Sized, - { - MapReparamCurve { - reparam_domain: domain, - base: self.preimage, - forward_map: self.f, - reparam_map: g, - _phantom: PhantomData, - } - } } -/// A [`Curve`] whose sample space is mapped onto that of some base curve's before sampling. +/// 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 -where - C: Curve, - F: Fn(f32) -> f32, -{ +pub struct ReparamCurve { domain: Interval, base: C, f: F, @@ -744,112 +724,68 @@ where fn sample(&self, t: f32) -> T { self.base.sample((self.f)(t)) } - - #[inline] - fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve - where - Self: Sized, - { - let fg = move |t| (self.f)(g(t)); - ReparamCurve { - domain, - base: self.base, - f: fg, - _phantom: PhantomData, - } - } - - #[inline] - fn map(self, g: impl Fn(T) -> S) -> impl Curve - where - Self: Sized, - { - MapReparamCurve { - reparam_domain: self.domain, - base: self.base, - forward_map: g, - reparam_map: self.f, - _phantom: PhantomData, - } - } } -/// A [`Curve`] structure that holds both forward and backward remapping information -/// in order to optimize repeated calls of [`Curve::map`] and [`Curve::reparametrize`]. -/// -/// Briefly, the point is that the curve just absorbs new functions instead of rebasing -/// itself inside new structs. +/// 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 MapReparamCurve -where - C: Curve, - F: Fn(S) -> T, - G: Fn(f32) -> f32, -{ - reparam_domain: Interval, +pub struct LinearReparamCurve { base: C, - forward_map: F, - reparam_map: G, - _phantom: PhantomData<(S, T)>, + /// Invariants: This interval must always be bounded. + new_domain: Interval, + _phantom: PhantomData, } -impl Curve for MapReparamCurve +impl Curve for LinearReparamCurve where - C: Curve, - F: Fn(S) -> T, - G: Fn(f32) -> f32, + C: Curve, { #[inline] fn domain(&self) -> Interval { - self.reparam_domain + self.new_domain } #[inline] fn sample(&self, t: f32) -> T { - (self.forward_map)(self.base.sample((self.reparam_map)(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 map(self, g: impl Fn(T) -> R) -> impl Curve - where - Self: Sized, - { - let gf = move |x| g((self.forward_map)(x)); - MapReparamCurve { - reparam_domain: self.reparam_domain, - base: self.base, - forward_map: gf, - reparam_map: self.reparam_map, - _phantom: PhantomData, - } + fn domain(&self) -> Interval { + self.reparam_curve.domain() } #[inline] - fn reparametrize(self, domain: Interval, g: impl Fn(f32) -> f32) -> impl Curve - where - Self: Sized, - { - let fg = move |t| (self.reparam_map)(g(t)); - MapReparamCurve { - reparam_domain: domain, - base: self.base, - forward_map: self.forward_map, - reparam_map: fg, - _phantom: PhantomData, - } + 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. +/// 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 -where - C: Curve, -{ +pub struct GraphCurve { base: C, _phantom: PhantomData, } @@ -869,15 +805,11 @@ where } } -/// A [`Curve`] that combines the data from two constituent curves into a tuple output type. +/// 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 -where - C: Curve, - D: Curve, -{ +pub struct ProductCurve { domain: Interval, first: C, second: D, @@ -900,16 +832,12 @@ where } } -/// The [`Curve`] that results from composing one curve with another. The second curve is +/// The curve that results from composing 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 ComposeCurve -where - C: Curve, - D: Curve, -{ +pub struct ComposeCurve { first: C, second: D, _phantom: PhantomData, @@ -945,17 +873,21 @@ where } /// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. -pub fn constant_curve(domain: Interval, value: T) -> impl Curve { +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) -> impl Curve +pub fn function_curve(domain: Interval, f: F) -> FunctionCurve where F: Fn(f32) -> T, { - FunctionCurve { domain, f } + FunctionCurve { + domain, + f, + _phantom: PhantomData, + } } /// Flip a curve that outputs tuples so that the tuples are arranged the other way. From 08001990d8cb62e7ed1bad03cad3f49dd3110398 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 17 Jun 2024 17:03:13 -0400 Subject: [PATCH 27/28] Fix object safety --- crates/bevy_math/src/curve/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 1f6cdb7624085..b4e7c917e0946 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -122,7 +122,10 @@ pub trait Curve { /// 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> { + fn samples(&self, samples: usize) -> Result, ResamplingError> + where + Self: Sized, + { if samples < 2 { return Err(ResamplingError::NotEnoughSamples(samples)); } From 53cd548dbfa923eeda10c13d6015968087defc3c Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Fri, 28 Jun 2024 11:41:22 -0400 Subject: [PATCH 28/28] Leftover renaming in cores API --- crates/bevy_math/src/curve/cores.rs | 47 ++++++++++++++--------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 2e3e97b245d93..101ce877c8929 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -168,7 +168,7 @@ impl EvenCore { T: Clone, I: Fn(&T, &T, f32) -> T, { - match even_betweenness(self.domain, self.samples.len(), t) { + match even_interp(self.domain, self.samples.len(), t) { InterpolationDatum::Exact(idx) | InterpolationDatum::LeftTail(idx) | InterpolationDatum::RightTail(idx) => self.samples[idx].clone(), @@ -184,17 +184,17 @@ impl EvenCore { /// variants give additional context about where the value is relative to the family of samples. /// /// [`Between`]: `InterpolationDatum::Between` - pub fn sample_betweenness(&self, t: f32) -> InterpolationDatum<&T> { - even_betweenness(self.domain, self.samples.len(), t).map(|idx| &self.samples[idx]) + pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&T> { + even_interp(self.domain, self.samples.len(), t).map(|idx| &self.samples[idx]) } - /// Like [`sample_betweenness`], but the returned values include the sample times. This can be + /// Like [`sample_interp`], but the returned values include the sample times. This can be /// useful when sampling is not scale-invariant. /// - /// [`sample_betweenness`]: EvenCore::sample_betweenness - pub fn sample_betweenness_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> { + /// [`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_betweenness(self.domain, self.samples.len(), t).map(|idx| { + even_interp(self.domain, self.samples.len(), t).map(|idx| { ( self.domain.start() + segment_len * idx as f32, &self.samples[idx], @@ -211,7 +211,7 @@ impl EvenCore { /// `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_betweenness(domain: Interval, samples: usize, t: f32) -> InterpolationDatum { +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(); @@ -323,7 +323,7 @@ impl UnevenCore { T: Clone, I: Fn(&T, &T, f32) -> T, { - match uneven_betweenness(&self.times, t) { + match uneven_interp(&self.times, t) { InterpolationDatum::Exact(idx) | InterpolationDatum::LeftTail(idx) | InterpolationDatum::RightTail(idx) => self.samples[idx].clone(), @@ -339,16 +339,16 @@ impl UnevenCore { /// variants give additional context about where the value is relative to the family of samples. /// /// [`Between`]: `InterpolationDatum::Between` - pub fn sample_betweenness(&self, t: f32) -> InterpolationDatum<&T> { - uneven_betweenness(&self.times, t).map(|idx| &self.samples[idx]) + pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&T> { + uneven_interp(&self.times, t).map(|idx| &self.samples[idx]) } - /// Like [`sample_betweenness`], but the returned values include the sample times. This can be + /// Like [`sample_interp`], but the returned values include the sample times. This can be /// useful when sampling is not scale-invariant. /// - /// [`sample_betweenness`]: UnevenCore::sample_betweenness - pub fn sample_betweenness_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> { - uneven_betweenness(&self.times, t).map(|idx| (self.times[idx], &self.samples[idx])) + /// [`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`. @@ -484,17 +484,16 @@ impl ChunkedUnevenCore { /// /// [`Between`]: `InterpolationDatum::Between` #[inline] - pub fn sample_betweenness(&self, t: f32) -> InterpolationDatum<&[T]> { - uneven_betweenness(&self.times, t).map(|idx| self.time_index_to_slice(idx)) + pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&[T]> { + uneven_interp(&self.times, t).map(|idx| self.time_index_to_slice(idx)) } - /// Like [`sample_betweenness`], but the returned values include the sample times. This can be + /// Like [`sample_interp`], but the returned values include the sample times. This can be /// useful when sampling is not scale-invariant. /// - /// [`sample_betweenness`]: ChunkedUnevenCore::sample_betweenness - pub fn sample_betweenness_timed(&self, t: f32) -> InterpolationDatum<(f32, &[T])> { - uneven_betweenness(&self.times, t) - .map(|idx| (self.times[idx], self.time_index_to_slice(idx))) + /// [`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 @@ -520,7 +519,7 @@ fn filter_sort_dedup_times(times: Vec) -> Vec { times } -/// Given a list of `times` and a target value, get the betweenness relationship for the +/// 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. /// @@ -529,7 +528,7 @@ fn filter_sort_dedup_times(times: Vec) -> Vec { /// /// # Panics /// This function will panic if `times` contains NAN. -pub fn uneven_betweenness(times: &[f32], t: f32) -> InterpolationDatum { +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) => {