Refactor non-core Curve methods into extension traits (#16930)
# Objective The way `Curve` presently achieves dyn-compatibility involves shoving `Self: Sized` bounds on a bunch of methods to forbid them from appearing in vtables. (This is called *explicit non-dispatchability*.) The `Curve` trait probably also just has way too many methods on its own. In the past, using extension traits instead to achieve similar functionality has been discussed. The upshot is that this would allow the "core" of the curve trait, on which all the automatic methods rely, to live in a very simple dyn-compatible trait, while other functionality is implemented by extensions. For instance, `dyn Curve<T>` cannot use the `Sized` methods, but `Box<dyn Curve<T>>` is `Sized`, hence would automatically implement the extension trait, containing the methods which are currently non-dispatchable. Other motivations for this include modularity and code organization: the `Curve` trait itself has grown quite large with the addition of numerous adaptors, and refactoring it to demonstrate the separation of functionality that is already present makes a lot of sense. Furthermore, resampling behavior in particular is dependent on special traits that may be mimicked or analogized in user-space, and creating extension traits to achieve similar behavior in user-space is something we ought to encourage by example. ## Solution `Curve` now contains only `domain` and the `sample` methods. `CurveExt` has been created, and it contains all adaptors, along with the other sampling convenience methods (`samples`, `sample_iter`, etc.). It is implemented for all `C` where `C: Curve<T> + Sized`. `CurveResampleExt` has been created, and it contains all resampling methods. It is implemented for all `C` where `C: Curve<T> + ?Sized`. ## Testing It compiles and `cargo doc` succeeds. --- ## Future work - Consider writing extension traits for resampling curves in related domains (e.g. resampling for `Curve<T>` where `T: Animatable` into an `AnimatableKeyframeCurve`). - `CurveExt` might be further broken down to separate the adaptor and sampling methods. --- ## Migration Guide `Curve` has been refactored so that much of its functionality is now in extension traits. Adaptors such as `map`, `reparametrize`, `reverse`, and so on now require importing `CurveExt`, while the resampling methods `resample_*` require importing `CurveResampleExt`. Both of these new traits are exported through `bevy::math::curve` and through `bevy::math::prelude`.
This commit is contained in:
parent
31367108f7
commit
97909df6c0
@ -76,6 +76,7 @@ where
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{palettes::basic, Srgba};
|
||||
use bevy_math::curve::{Curve, CurveExt};
|
||||
|
||||
#[test]
|
||||
fn test_color_curve() {
|
||||
|
@ -4,7 +4,10 @@
|
||||
//! [`GizmoBuffer::curve_3d`] and assorted support items.
|
||||
|
||||
use bevy_color::Color;
|
||||
use bevy_math::{curve::Curve, Vec2, Vec3};
|
||||
use bevy_math::{
|
||||
curve::{Curve, CurveExt},
|
||||
Vec2, Vec3,
|
||||
};
|
||||
|
||||
use crate::{gizmos::GizmoBuffer, prelude::GizmoConfigGroup};
|
||||
|
||||
|
@ -18,6 +18,9 @@ mod paths {
|
||||
pub(super) const THIS_CRATE: &str = "bevy_math";
|
||||
}
|
||||
|
||||
#[expect(unused, reason = "imported just for doc links")]
|
||||
use super::CurveExt;
|
||||
|
||||
// NOTE ON REFLECTION:
|
||||
//
|
||||
// Function members of structs pose an obstacle for reflection, because they don't implement
|
||||
@ -173,7 +176,7 @@ where
|
||||
}
|
||||
|
||||
/// A curve whose samples are defined by mapping samples from another curve through a
|
||||
/// given function. Curves of this type are produced by [`Curve::map`].
|
||||
/// given function. Curves of this type are produced by [`CurveExt::map`].
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(
|
||||
@ -269,7 +272,7 @@ where
|
||||
}
|
||||
|
||||
/// A curve whose sample space is mapped onto that of some base curve's before sampling.
|
||||
/// Curves of this type are produced by [`Curve::reparametrize`].
|
||||
/// Curves of this type are produced by [`CurveExt::reparametrize`].
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(
|
||||
@ -364,7 +367,7 @@ where
|
||||
}
|
||||
|
||||
/// A curve that has had its domain changed by a linear reparameterization (stretching and scaling).
|
||||
/// Curves of this type are produced by [`Curve::reparametrize_linear`].
|
||||
/// Curves of this type are produced by [`CurveExt::reparametrize_linear`].
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(
|
||||
@ -399,7 +402,7 @@ where
|
||||
}
|
||||
|
||||
/// A curve that has been reparametrized by another curve, using that curve to transform the
|
||||
/// sample times before sampling. Curves of this type are produced by [`Curve::reparametrize_by_curve`].
|
||||
/// sample times before sampling. Curves of this type are produced by [`CurveExt::reparametrize_by_curve`].
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(
|
||||
@ -432,7 +435,7 @@ where
|
||||
}
|
||||
|
||||
/// A curve that is the graph of another curve over its parameter space. Curves of this type are
|
||||
/// produced by [`Curve::graph`].
|
||||
/// produced by [`CurveExt::graph`].
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(
|
||||
@ -462,7 +465,7 @@ where
|
||||
}
|
||||
|
||||
/// A curve that combines the output data from two constituent curves into a tuple output. Curves
|
||||
/// of this type are produced by [`Curve::zip`].
|
||||
/// of this type are produced by [`CurveExt::zip`].
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(
|
||||
@ -503,7 +506,7 @@ where
|
||||
/// For this to be well-formed, the first curve's domain must be right-finite and the second's
|
||||
/// must be left-finite.
|
||||
///
|
||||
/// Curves of this type are produced by [`Curve::chain`].
|
||||
/// Curves of this type are produced by [`CurveExt::chain`].
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(
|
||||
@ -549,7 +552,7 @@ where
|
||||
|
||||
/// The curve that results from reversing another.
|
||||
///
|
||||
/// Curves of this type are produced by [`Curve::reverse`].
|
||||
/// Curves of this type are produced by [`CurveExt::reverse`].
|
||||
///
|
||||
/// # Domain
|
||||
///
|
||||
@ -590,7 +593,7 @@ where
|
||||
/// - the value at the transitioning points (`domain.end() * n` for `n >= 1`) in the results is the
|
||||
/// value at `domain.end()` in the original curve
|
||||
///
|
||||
/// Curves of this type are produced by [`Curve::repeat`].
|
||||
/// Curves of this type are produced by [`CurveExt::repeat`].
|
||||
///
|
||||
/// # Domain
|
||||
///
|
||||
@ -649,7 +652,7 @@ where
|
||||
/// - the value at the transitioning points (`domain.end() * n` for `n >= 1`) in the results is the
|
||||
/// value at `domain.end()` in the original curve
|
||||
///
|
||||
/// Curves of this type are produced by [`Curve::forever`].
|
||||
/// Curves of this type are produced by [`CurveExt::forever`].
|
||||
///
|
||||
/// # Domain
|
||||
///
|
||||
@ -703,7 +706,7 @@ where
|
||||
/// The curve that results from chaining a curve with its reversed version. The transition point
|
||||
/// is guaranteed to make no jump.
|
||||
///
|
||||
/// Curves of this type are produced by [`Curve::ping_pong`].
|
||||
/// Curves of this type are produced by [`CurveExt::ping_pong`].
|
||||
///
|
||||
/// # Domain
|
||||
///
|
||||
@ -756,7 +759,7 @@ where
|
||||
/// realized by translating the second curve so that its start sample point coincides with the
|
||||
/// first curves' end sample point.
|
||||
///
|
||||
/// Curves of this type are produced by [`Curve::chain_continue`].
|
||||
/// Curves of this type are produced by [`CurveExt::chain_continue`].
|
||||
///
|
||||
/// # Domain
|
||||
///
|
||||
|
@ -432,14 +432,14 @@ impl<T> UnevenCore<T> {
|
||||
}
|
||||
|
||||
/// This core, but with the sample times moved by the map `f`.
|
||||
/// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`],
|
||||
/// In principle, when `f` is monotone, this is equivalent to [`CurveExt::reparametrize`],
|
||||
/// but the function inputs to each are inverses of one another.
|
||||
///
|
||||
/// The samples are re-sorted by time after mapping and deduplicated by output time, so
|
||||
/// the function `f` should generally be injective over the set of sample times, otherwise
|
||||
/// data will be deleted.
|
||||
///
|
||||
/// [`Curve::reparametrize`]: crate::curve::Curve::reparametrize
|
||||
/// [`CurveExt::reparametrize`]: crate::curve::CurveExt::reparametrize
|
||||
#[must_use]
|
||||
pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenCore<T> {
|
||||
let mut timed_samples = self
|
||||
|
@ -453,7 +453,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::cubic_splines::{CubicBezier, CubicCardinalSpline, CubicCurve, CubicGenerator};
|
||||
use crate::curve::{Curve, Interval};
|
||||
use crate::curve::{Curve, CurveExt, Interval};
|
||||
use crate::{vec2, Vec2, Vec3};
|
||||
|
||||
fn test_curve() -> CubicCurve<Vec2> {
|
||||
|
@ -20,7 +20,7 @@
|
||||
//! counterpart.
|
||||
//!
|
||||
//! [`with_derivative`]: CurveWithDerivative::with_derivative
|
||||
//! [`by_ref`]: Curve::by_ref
|
||||
//! [`by_ref`]: crate::curve::CurveExt::by_ref
|
||||
|
||||
pub mod adaptor_impls;
|
||||
|
||||
|
@ -4,8 +4,8 @@
|
||||
//! [easing functions]: EaseFunction
|
||||
|
||||
use crate::{
|
||||
curve::{FunctionCurve, Interval},
|
||||
Curve, Dir2, Dir3, Dir3A, Quat, Rot2, VectorSpace,
|
||||
curve::{Curve, CurveExt, FunctionCurve, Interval},
|
||||
Dir2, Dir3, Dir3A, Quat, Rot2, VectorSpace,
|
||||
};
|
||||
|
||||
use variadics_please::all_tuples_enumerated;
|
||||
|
@ -269,18 +269,18 @@
|
||||
//!
|
||||
//! [domain]: Curve::domain
|
||||
//! [sampled]: Curve::sample
|
||||
//! [changing parametrizations]: Curve::reparametrize
|
||||
//! [mapping output]: Curve::map
|
||||
//! [rasterization]: Curve::resample
|
||||
//! [changing parametrizations]: CurveExt::reparametrize
|
||||
//! [mapping output]: CurveExt::map
|
||||
//! [rasterization]: CurveResampleExt::resample
|
||||
//! [functions]: FunctionCurve
|
||||
//! [sample interpolation]: SampleCurve
|
||||
//! [splines]: crate::cubic_splines
|
||||
//! [easings]: easing
|
||||
//! [spline curves]: crate::cubic_splines
|
||||
//! [easing curves]: easing
|
||||
//! [`chain`]: Curve::chain
|
||||
//! [`zip`]: Curve::zip
|
||||
//! [`resample`]: Curve::resample
|
||||
//! [`chain`]: CurveExt::chain
|
||||
//! [`zip`]: CurveExt::zip
|
||||
//! [`resample`]: CurveResampleExt::resample
|
||||
//!
|
||||
//! [^footnote]: In fact, universal as well, in some sense: if `curve` is any curve, then `FunctionCurve::new
|
||||
//! (curve.domain(), |t| curve.sample_unchecked(t))` is an equivalent function curve.
|
||||
@ -302,9 +302,7 @@ pub use interval::{interval, Interval};
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
pub use {
|
||||
crate::StableInterpolate,
|
||||
cores::{EvenCore, UnevenCore},
|
||||
itertools::Itertools,
|
||||
sample_curves::*,
|
||||
};
|
||||
|
||||
@ -313,6 +311,9 @@ use core::{marker::PhantomData, ops::Deref};
|
||||
use interval::InvalidIntervalError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
use {crate::StableInterpolate, itertools::Itertools};
|
||||
|
||||
/// 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
|
||||
@ -349,17 +350,41 @@ pub trait Curve<T> {
|
||||
let t = self.domain().clamp(t);
|
||||
self.sample_unchecked(t)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C, D> Curve<T> for D
|
||||
where
|
||||
C: Curve<T> + ?Sized,
|
||||
D: Deref<Target = C>,
|
||||
{
|
||||
fn domain(&self) -> Interval {
|
||||
<C as Curve<T>>::domain(self)
|
||||
}
|
||||
|
||||
fn sample_unchecked(&self, t: f32) -> T {
|
||||
<C as Curve<T>>::sample_unchecked(self, t)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait implemented by [curves], allowing access to a number of adaptors and
|
||||
/// convenience methods.
|
||||
///
|
||||
/// This trait is automatically implemented for all curves that are `Sized`. In particular,
|
||||
/// it is implemented for types like `Box<dyn Curve<T>>`. `CurveExt` is not dyn-compatible
|
||||
/// itself.
|
||||
///
|
||||
/// For more information, see the [module-level documentation].
|
||||
///
|
||||
/// [curves]: Curve
|
||||
/// [module-level documentation]: self
|
||||
pub trait CurveExt<T>: Curve<T> + Sized {
|
||||
/// Sample a collection of `n >= 0` points on this curve at the parameter values `t_n`,
|
||||
/// returning `None` if the point is outside of the curve's domain.
|
||||
///
|
||||
/// The samples are returned in the same order as the parameter values `t_n` were provided and
|
||||
/// will include all results. This leaves the responsibility for things like filtering and
|
||||
/// sorting to the user for maximum flexibility.
|
||||
fn sample_iter(&self, iter: impl IntoIterator<Item = f32>) -> impl Iterator<Item = Option<T>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn sample_iter(&self, iter: impl IntoIterator<Item = f32>) -> impl Iterator<Item = Option<T>> {
|
||||
iter.into_iter().map(|t| self.sample(t))
|
||||
}
|
||||
|
||||
@ -374,10 +399,10 @@ pub trait Curve<T> {
|
||||
/// The samples are returned in the same order as the parameter values `t_n` were provided and
|
||||
/// will include all results. This leaves the responsibility for things like filtering and
|
||||
/// sorting to the user for maximum flexibility.
|
||||
fn sample_iter_unchecked(&self, iter: impl IntoIterator<Item = f32>) -> impl Iterator<Item = T>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn sample_iter_unchecked(
|
||||
&self,
|
||||
iter: impl IntoIterator<Item = f32>,
|
||||
) -> impl Iterator<Item = T> {
|
||||
iter.into_iter().map(|t| self.sample_unchecked(t))
|
||||
}
|
||||
|
||||
@ -387,10 +412,7 @@ pub trait Curve<T> {
|
||||
/// The samples are returned in the same order as the parameter values `t_n` were provided and
|
||||
/// will include all results. This leaves the responsibility for things like filtering and
|
||||
/// sorting to the user for maximum flexibility.
|
||||
fn sample_iter_clamped(&self, iter: impl IntoIterator<Item = f32>) -> impl Iterator<Item = T>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn sample_iter_clamped(&self, iter: impl IntoIterator<Item = f32>) -> impl Iterator<Item = T> {
|
||||
iter.into_iter().map(|t| self.sample_clamped(t))
|
||||
}
|
||||
|
||||
@ -400,7 +422,6 @@ pub trait Curve<T> {
|
||||
#[must_use]
|
||||
fn map<S, F>(self, f: F) -> MapCurve<T, S, Self, F>
|
||||
where
|
||||
Self: Sized,
|
||||
F: Fn(T) -> S,
|
||||
{
|
||||
MapCurve {
|
||||
@ -425,7 +446,7 @@ pub trait Curve<T> {
|
||||
/// let scaled_curve = my_curve.reparametrize(interval(0.0, 2.0).unwrap(), |t| t / 2.0);
|
||||
/// ```
|
||||
/// This kind of linear remapping is provided by the convenience method
|
||||
/// [`Curve::reparametrize_linear`], which requires only the desired domain for the new curve.
|
||||
/// [`CurveExt::reparametrize_linear`], which requires only the desired domain for the new curve.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
@ -443,7 +464,6 @@ pub trait Curve<T> {
|
||||
#[must_use]
|
||||
fn reparametrize<F>(self, domain: Interval, f: F) -> ReparamCurve<T, Self, F>
|
||||
where
|
||||
Self: Sized,
|
||||
F: Fn(f32) -> f32,
|
||||
{
|
||||
ReparamCurve {
|
||||
@ -456,15 +476,15 @@ pub trait Curve<T> {
|
||||
|
||||
/// 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 bounded
|
||||
/// domains; if either this curve's domain or the given `domain` is unbounded, an error is
|
||||
/// returned.
|
||||
/// domains.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If either this curve's domain or the given `domain` is unbounded, an error is returned.
|
||||
fn reparametrize_linear(
|
||||
self,
|
||||
domain: Interval,
|
||||
) -> Result<LinearReparamCurve<T, Self>, LinearReparamError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
) -> Result<LinearReparamCurve<T, Self>, LinearReparamError> {
|
||||
if !self.domain().is_bounded() {
|
||||
return Err(LinearReparamError::SourceCurveUnbounded);
|
||||
}
|
||||
@ -488,7 +508,6 @@ pub trait Curve<T> {
|
||||
#[must_use]
|
||||
fn reparametrize_by_curve<C>(self, other: C) -> CurveReparamCurve<T, Self, C>
|
||||
where
|
||||
Self: Sized,
|
||||
C: Curve<f32>,
|
||||
{
|
||||
CurveReparamCurve {
|
||||
@ -505,10 +524,7 @@ pub trait Curve<T> {
|
||||
/// `(t, x)` at time `t`. In particular, if this curve is a `Curve<T>`, the output of this method
|
||||
/// is a `Curve<(f32, T)>`.
|
||||
#[must_use]
|
||||
fn graph(self) -> GraphCurve<T, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn graph(self) -> GraphCurve<T, Self> {
|
||||
GraphCurve {
|
||||
base: self,
|
||||
_phantom: PhantomData,
|
||||
@ -519,11 +535,13 @@ pub trait Curve<T> {
|
||||
///
|
||||
/// 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 error is returned.
|
||||
/// intersection of the domains of its constituents.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If the domain intersection would be empty, an error is returned instead.
|
||||
fn zip<S, C>(self, other: C) -> Result<ZipCurve<T, S, Self, C>, InvalidIntervalError>
|
||||
where
|
||||
Self: Sized,
|
||||
C: Curve<S> + Sized,
|
||||
{
|
||||
let domain = self.domain().intersect(other.domain())?;
|
||||
@ -545,7 +563,6 @@ pub trait Curve<T> {
|
||||
/// `other`'s domain doesn't have a finite start.
|
||||
fn chain<C>(self, other: C) -> Result<ChainCurve<T, Self, C>, ChainError>
|
||||
where
|
||||
Self: Sized,
|
||||
C: Curve<T>,
|
||||
{
|
||||
if !self.domain().has_finite_end() {
|
||||
@ -566,13 +583,10 @@ pub trait Curve<T> {
|
||||
/// and transitioning over to `self.domain().start()`. The domain of the new curve is still the
|
||||
/// same.
|
||||
///
|
||||
/// # Error
|
||||
/// # Errors
|
||||
///
|
||||
/// A [`ReverseError`] is returned if this curve's domain isn't bounded.
|
||||
fn reverse(self) -> Result<ReverseCurve<T, Self>, ReverseError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn reverse(self) -> Result<ReverseCurve<T, Self>, ReverseError> {
|
||||
self.domain()
|
||||
.is_bounded()
|
||||
.then(|| ReverseCurve {
|
||||
@ -593,13 +607,10 @@ pub trait Curve<T> {
|
||||
/// - the value at the transitioning points (`domain.end() * n` for `n >= 1`) in the results is the
|
||||
/// value at `domain.end()` in the original curve
|
||||
///
|
||||
/// # Error
|
||||
/// # Errors
|
||||
///
|
||||
/// A [`RepeatError`] is returned if this curve's domain isn't bounded.
|
||||
fn repeat(self, count: usize) -> Result<RepeatCurve<T, Self>, RepeatError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn repeat(self, count: usize) -> Result<RepeatCurve<T, Self>, RepeatError> {
|
||||
self.domain()
|
||||
.is_bounded()
|
||||
.then(|| {
|
||||
@ -629,13 +640,10 @@ pub trait Curve<T> {
|
||||
/// - the value at the transitioning points (`domain.end() * n` for `n >= 1`) in the results is the
|
||||
/// value at `domain.end()` in the original curve
|
||||
///
|
||||
/// # Error
|
||||
/// # Errors
|
||||
///
|
||||
/// A [`RepeatError`] is returned if this curve's domain isn't bounded.
|
||||
fn forever(self) -> Result<ForeverCurve<T, Self>, RepeatError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn forever(self) -> Result<ForeverCurve<T, Self>, RepeatError> {
|
||||
self.domain()
|
||||
.is_bounded()
|
||||
.then(|| ForeverCurve {
|
||||
@ -649,13 +657,10 @@ pub trait Curve<T> {
|
||||
/// another curve with outputs of the same type. The domain of the new curve will be twice as
|
||||
/// long. The transition point is guaranteed to not make any jumps.
|
||||
///
|
||||
/// # Error
|
||||
/// # Errors
|
||||
///
|
||||
/// A [`PingPongError`] is returned if this curve's domain isn't right-finite.
|
||||
fn ping_pong(self) -> Result<PingPongCurve<T, Self>, PingPongError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn ping_pong(self) -> Result<PingPongCurve<T, Self>, PingPongError> {
|
||||
self.domain()
|
||||
.has_finite_end()
|
||||
.then(|| PingPongCurve {
|
||||
@ -676,13 +681,12 @@ pub trait Curve<T> {
|
||||
/// realized by translating the other curve so that its start sample point coincides with the
|
||||
/// current curves' end sample point.
|
||||
///
|
||||
/// # Error
|
||||
/// # Errors
|
||||
///
|
||||
/// A [`ChainError`] is returned if this curve's domain doesn't have a finite end or if
|
||||
/// `other`'s domain doesn't have a finite start.
|
||||
fn chain_continue<C>(self, other: C) -> Result<ContinuationCurve<T, Self, C>, ChainError>
|
||||
where
|
||||
Self: Sized,
|
||||
T: VectorSpace,
|
||||
C: Curve<T>,
|
||||
{
|
||||
@ -704,74 +708,13 @@ pub trait Curve<T> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally
|
||||
/// spaced sample values, using the provided `interpolation` to interpolate between adjacent samples.
|
||||
/// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1,
|
||||
/// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at
|
||||
/// the midpoint is taken as well, and so on. If `segments` is zero, or if this curve has an unbounded
|
||||
/// domain, then a [`ResamplingError`] is returned.
|
||||
/// Extract an iterator over evenly-spaced samples from this curve.
|
||||
///
|
||||
/// 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.
|
||||
/// # Errors
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use bevy_math::*;
|
||||
/// # use bevy_math::curve::*;
|
||||
/// let quarter_rotation = FunctionCurve::new(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));
|
||||
/// ```
|
||||
#[cfg(feature = "alloc")]
|
||||
fn resample<I>(
|
||||
&self,
|
||||
segments: usize,
|
||||
interpolation: I,
|
||||
) -> Result<SampleCurve<T, I>, ResamplingError>
|
||||
where
|
||||
Self: Sized,
|
||||
I: Fn(&T, &T, f32) -> T,
|
||||
{
|
||||
let samples = self.samples(segments + 1)?.collect_vec();
|
||||
Ok(SampleCurve {
|
||||
core: EvenCore {
|
||||
domain: self.domain(),
|
||||
samples,
|
||||
},
|
||||
interpolation,
|
||||
})
|
||||
}
|
||||
|
||||
/// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally
|
||||
/// spaced sample values, using [automatic interpolation] to interpolate between adjacent samples.
|
||||
/// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1,
|
||||
/// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at
|
||||
/// the midpoint is taken as well, and so on. If `segments` is zero, or if this curve has an unbounded
|
||||
/// domain, then a [`ResamplingError`] is returned.
|
||||
///
|
||||
/// [automatic interpolation]: crate::common_traits::StableInterpolate
|
||||
#[cfg(feature = "alloc")]
|
||||
fn resample_auto(&self, segments: usize) -> Result<SampleAutoCurve<T>, ResamplingError>
|
||||
where
|
||||
Self: Sized,
|
||||
T: StableInterpolate,
|
||||
{
|
||||
let samples = self.samples(segments + 1)?.collect_vec();
|
||||
Ok(SampleAutoCurve {
|
||||
core: EvenCore {
|
||||
domain: self.domain(),
|
||||
samples,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract an iterator over evenly-spaced samples from this curve. If `samples` is less than 2
|
||||
/// or if this curve has unbounded domain, then an error is returned instead.
|
||||
fn samples(&self, samples: usize) -> Result<impl Iterator<Item = T>, ResamplingError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
/// If `samples` is less than 2 or if this curve has unbounded domain, a [`ResamplingError`]
|
||||
/// is returned.
|
||||
fn samples(&self, samples: usize) -> Result<impl Iterator<Item = T>, ResamplingError> {
|
||||
if samples < 2 {
|
||||
return Err(ResamplingError::NotEnoughSamples(samples));
|
||||
}
|
||||
@ -788,13 +731,122 @@ pub trait Curve<T> {
|
||||
.map(|t| self.sample_unchecked(t)))
|
||||
}
|
||||
|
||||
/// 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 = FunctionCurve::new(Interval::UNIT, |t| t * t + 1.0);
|
||||
///
|
||||
/// // Borrow `my_curve` long enough to resample a mapped version. Note that `map` takes
|
||||
/// // ownership of its input.
|
||||
/// let samples = my_curve.by_ref().map(|x| x * 2.0).resample_auto(100).unwrap();
|
||||
///
|
||||
/// // Do something else with `my_curve` since we retained ownership:
|
||||
/// let new_curve = my_curve.reparametrize_linear(interval(-1.0, 1.0).unwrap()).unwrap();
|
||||
/// ```
|
||||
fn by_ref(&self) -> &Self {
|
||||
self
|
||||
}
|
||||
|
||||
/// Flip this curve so that its tuple output is arranged the other way.
|
||||
#[must_use]
|
||||
fn flip<U, V>(self) -> impl Curve<(V, U)>
|
||||
where
|
||||
Self: CurveExt<(U, V)>,
|
||||
{
|
||||
self.map(|(u, v)| (v, u))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, T> CurveExt<T> for C where C: Curve<T> {}
|
||||
|
||||
/// Extension trait implemented by [curves], allowing access to generic resampling methods as
|
||||
/// well as those based on [stable interpolation].
|
||||
///
|
||||
/// This trait is automatically implemented for all curves.
|
||||
///
|
||||
/// For more information, see the [module-level documentation].
|
||||
///
|
||||
/// [curves]: Curve
|
||||
/// [stable interpolation]: crate::StableInterpolate
|
||||
/// [module-level documentation]: self
|
||||
#[cfg(feature = "alloc")]
|
||||
pub trait CurveResampleExt<T>: Curve<T> {
|
||||
/// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally
|
||||
/// spaced sample values, using the provided `interpolation` to interpolate between adjacent samples.
|
||||
/// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1,
|
||||
/// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at
|
||||
/// the midpoint is taken as well, and so on.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If `segments` is zero or if this curve has unbounded domain, then a [`ResamplingError`] is
|
||||
/// returned.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use bevy_math::*;
|
||||
/// # use bevy_math::curve::*;
|
||||
/// let quarter_rotation = FunctionCurve::new(interval(0.0, 90.0).unwrap(), |t| Rot2::degrees(t));
|
||||
/// // A curve which only stores three data points and uses `nlerp` to interpolate them:
|
||||
/// let resampled_rotation = quarter_rotation.resample(3, |x, y, t| x.nlerp(*y, t));
|
||||
/// ```
|
||||
fn resample<I>(
|
||||
&self,
|
||||
segments: usize,
|
||||
interpolation: I,
|
||||
) -> Result<SampleCurve<T, I>, ResamplingError>
|
||||
where
|
||||
I: Fn(&T, &T, f32) -> T,
|
||||
{
|
||||
let samples = self.samples(segments + 1)?.collect_vec();
|
||||
Ok(SampleCurve {
|
||||
core: EvenCore {
|
||||
domain: self.domain(),
|
||||
samples,
|
||||
},
|
||||
interpolation,
|
||||
})
|
||||
}
|
||||
|
||||
/// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally
|
||||
/// spaced sample values, using [automatic interpolation] to interpolate between adjacent samples.
|
||||
/// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1,
|
||||
/// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at
|
||||
/// the midpoint is taken as well, and so on.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If `segments` is zero or if this curve has unbounded domain, a [`ResamplingError`] is returned.
|
||||
///
|
||||
/// [automatic interpolation]: crate::common_traits::StableInterpolate
|
||||
fn resample_auto(&self, segments: usize) -> Result<SampleAutoCurve<T>, ResamplingError>
|
||||
where
|
||||
T: StableInterpolate,
|
||||
{
|
||||
let samples = self.samples(segments + 1)?.collect_vec();
|
||||
Ok(SampleAutoCurve {
|
||||
core: EvenCore {
|
||||
domain: self.domain(),
|
||||
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
|
||||
/// are 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
|
||||
@ -803,14 +855,17 @@ pub trait Curve<T> {
|
||||
/// 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.
|
||||
#[cfg(feature = "alloc")]
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If `sample_times` doesn't contain at least two distinct times after filtering, a
|
||||
/// [`ResamplingError`] is returned.
|
||||
fn resample_uneven<I>(
|
||||
&self,
|
||||
sample_times: impl IntoIterator<Item = f32>,
|
||||
interpolation: I,
|
||||
) -> Result<UnevenSampleCurve<T, I>, ResamplingError>
|
||||
where
|
||||
Self: Sized,
|
||||
I: Fn(&T, &T, f32) -> T,
|
||||
{
|
||||
let domain = self.domain();
|
||||
@ -841,14 +896,17 @@ pub trait Curve<T> {
|
||||
/// The domain of the produced [`UnevenSampleAutoCurve`] stretches between the first and last
|
||||
/// sample times of the iterator.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If `sample_times` doesn't contain at least two distinct times after filtering, a
|
||||
/// [`ResamplingError`] is returned.
|
||||
///
|
||||
/// [automatic interpolation]: crate::common_traits::StableInterpolate
|
||||
#[cfg(feature = "alloc")]
|
||||
fn resample_uneven_auto(
|
||||
&self,
|
||||
sample_times: impl IntoIterator<Item = f32>,
|
||||
) -> Result<UnevenSampleAutoCurve<T>, ResamplingError>
|
||||
where
|
||||
Self: Sized,
|
||||
T: StableInterpolate,
|
||||
{
|
||||
let domain = self.domain();
|
||||
@ -866,53 +924,10 @@ pub trait Curve<T> {
|
||||
core: UnevenCore { times, samples },
|
||||
})
|
||||
}
|
||||
|
||||
/// Borrow this curve rather than taking ownership of it. This is essentially an alias for a
|
||||
/// prefix `&`; the point is that intermediate operations can be performed while retaining
|
||||
/// access to the original curve.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use bevy_math::curve::*;
|
||||
/// let my_curve = FunctionCurve::new(Interval::UNIT, |t| t * t + 1.0);
|
||||
///
|
||||
/// // Borrow `my_curve` long enough to resample a mapped version. Note that `map` takes
|
||||
/// // ownership of its input.
|
||||
/// let samples = my_curve.by_ref().map(|x| x * 2.0).resample_auto(100).unwrap();
|
||||
///
|
||||
/// // Do something else with `my_curve` since we retained ownership:
|
||||
/// let new_curve = my_curve.reparametrize_linear(interval(-1.0, 1.0).unwrap()).unwrap();
|
||||
/// ```
|
||||
fn by_ref(&self) -> &Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self
|
||||
}
|
||||
|
||||
/// Flip this curve so that its tuple output is arranged the other way.
|
||||
#[must_use]
|
||||
fn flip<U, V>(self) -> impl Curve<(V, U)>
|
||||
where
|
||||
Self: Sized + Curve<(U, V)>,
|
||||
{
|
||||
self.map(|(u, v)| (v, u))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C, D> Curve<T> for D
|
||||
where
|
||||
C: Curve<T> + ?Sized,
|
||||
D: Deref<Target = C>,
|
||||
{
|
||||
fn domain(&self) -> Interval {
|
||||
<C as Curve<T>>::domain(self)
|
||||
}
|
||||
|
||||
fn sample_unchecked(&self, t: f32) -> T {
|
||||
<C as Curve<T>>::sample_unchecked(self, t)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "alloc")]
|
||||
impl<C, T> CurveResampleExt<T> for C where C: Curve<T> + ?Sized {}
|
||||
|
||||
/// An error indicating that a linear reparameterization couldn't be performed because of
|
||||
/// malformed inputs.
|
||||
|
@ -285,11 +285,13 @@ impl<T, I> UnevenSampleCurve<T, I> {
|
||||
}
|
||||
|
||||
/// This [`UnevenSampleAutoCurve`], but with the sample times moved by the map `f`.
|
||||
/// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`],
|
||||
/// In principle, when `f` is monotone, this is equivalent to [`CurveExt::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.
|
||||
///
|
||||
/// [`CurveExt::reparametrize`]: super::CurveExt::reparametrize
|
||||
pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve<T, I> {
|
||||
Self {
|
||||
core: self.core.map_sample_times(f),
|
||||
@ -343,11 +345,13 @@ impl<T> UnevenSampleAutoCurve<T> {
|
||||
}
|
||||
|
||||
/// This [`UnevenSampleAutoCurve`], but with the sample times moved by the map `f`.
|
||||
/// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`],
|
||||
/// In principle, when `f` is monotone, this is equivalent to [`CurveExt::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.
|
||||
///
|
||||
/// [`CurveExt::reparametrize`]: super::CurveExt::reparametrize
|
||||
pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleAutoCurve<T> {
|
||||
Self {
|
||||
core: self.core.map_sample_times(f),
|
||||
|
Loading…
Reference in New Issue
Block a user