add more Curve adaptors (#14794)

# Objective

This implements another item on the way to complete the `Curves`
implementation initiative

Citing @mweatherley 

> Curve adaptors for making a curve repeat or ping-pong would be useful.

This adds three widely applicable adaptors:

- `ReverseCurve` "plays" the curve backwards
- `RepeatCurve` to repeat the curve for `n` times where `n` in `[0,inf)`
- `ForeverCurve` which extends the curves domain to `EVERYWHERE`
- `PingPongCurve` (name wip (?)) to chain the curve with it's reverse.
This would be achievable with `ReverseCurve` and `ChainCurve`, but it
would require the use of `by_ref` which can be restrictive in some
scenarios where you'd rather just consume the curve. Users can still
create the same effect by combination of the former two, but since this
will be most likely a very typical adaptor we should also provide it on
the library level. (Why it's typical: you can create a single period of
common waves with it pretty easily, think square wave (= pingpong +
step), triangle wave ( = pingpong + linear), etc.)
- `ContinuationCurve` which chains two curves but also makes sure that
the samples of the second curve are translated so that
`sample(first.end) == sample(second.start)`

## Solution

Implement the adaptors above. (More suggestions are welcome!)

## Testing

- [x] add simple tests. One per adaptor

---------

Co-authored-by: eckz <567737+eckz@users.noreply.github.com>
Co-authored-by: Matty <2975848+mweatherley@users.noreply.github.com>
Co-authored-by: IQuick 143 <IQuick143cz@gmail.com>
Co-authored-by: Matty <weatherleymatthew@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Robert Walter 2024-09-30 16:55:32 +00:00 committed by GitHub
parent 78a3aae81b
commit ff308488fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 552 additions and 66 deletions

View File

@ -0,0 +1,270 @@
//! A module containing utility helper structs to transform a [`Curve`] into another. This is useful
//! for building up complex curves from simple segments.
use core::marker::PhantomData;
use crate::VectorSpace;
use super::{Curve, Interval};
/// The curve that results from chaining one curve with another. The second curve is
/// effectively reparametrized so that its start is at the end of the first.
///
/// Curves of this type are produced by [`Curve::chain`].
///
/// # Domain
///
/// The first curve's domain must be right-finite and the second's must be left-finite to get a
/// valid [`ChainCurve`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct ChainCurve<T, C, D> {
pub(super) first: C,
pub(super) second: D,
pub(super) _phantom: PhantomData<T>,
}
impl<T, C, D> Curve<T> for ChainCurve<T, C, D>
where
C: Curve<T>,
D: Curve<T>,
{
#[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_unchecked(&self, t: f32) -> T {
if t > self.first.domain().end() {
self.second.sample_unchecked(
// `t - first.domain.end` computes the offset into the domain of the second.
t - self.first.domain().end() + self.second.domain().start(),
)
} else {
self.first.sample_unchecked(t)
}
}
}
/// The curve that results from reversing another.
///
/// Curves of this type are produced by [`Curve::reverse`].
///
/// # Domain
///
/// The original curve's domain must be bounded to get a valid [`ReverseCurve`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct ReverseCurve<T, C> {
pub(super) curve: C,
pub(super) _phantom: PhantomData<T>,
}
impl<T, C> Curve<T> for ReverseCurve<T, C>
where
C: Curve<T>,
{
#[inline]
fn domain(&self) -> Interval {
self.curve.domain()
}
#[inline]
fn sample_unchecked(&self, t: f32) -> T {
self.curve
.sample_unchecked(self.domain().end() - (t - self.domain().start()))
}
}
/// The curve that results from repeating a curve `N` times.
///
/// # Notes
///
/// - 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`].
///
/// # Domain
///
/// The original curve's domain must be bounded to get a valid [`RepeatCurve`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct RepeatCurve<T, C> {
pub(super) domain: Interval,
pub(super) curve: C,
pub(super) _phantom: PhantomData<T>,
}
impl<T, C> Curve<T> for RepeatCurve<T, C>
where
C: Curve<T>,
{
#[inline]
fn domain(&self) -> Interval {
self.domain
}
#[inline]
fn sample_unchecked(&self, t: f32) -> T {
// the domain is bounded by construction
let d = self.curve.domain();
let cyclic_t = (t - d.start()).rem_euclid(d.length());
let t = if t != d.start() && cyclic_t == 0.0 {
d.end()
} else {
d.start() + cyclic_t
};
self.curve.sample_unchecked(t)
}
}
/// The curve that results from repeating a curve forever.
///
/// # Notes
///
/// - 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`].
///
/// # Domain
///
/// The original curve's domain must be bounded to get a valid [`ForeverCurve`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct ForeverCurve<T, C> {
pub(super) curve: C,
pub(super) _phantom: PhantomData<T>,
}
impl<T, C> Curve<T> for ForeverCurve<T, C>
where
C: Curve<T>,
{
#[inline]
fn domain(&self) -> Interval {
Interval::EVERYWHERE
}
#[inline]
fn sample_unchecked(&self, t: f32) -> T {
// the domain is bounded by construction
let d = self.curve.domain();
let cyclic_t = (t - d.start()).rem_euclid(d.length());
let t = if t != d.start() && cyclic_t == 0.0 {
d.end()
} else {
d.start() + cyclic_t
};
self.curve.sample_unchecked(t)
}
}
/// 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`].
///
/// # Domain
///
/// The original curve's domain must be right-finite to get a valid [`PingPongCurve`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct PingPongCurve<T, C> {
pub(super) curve: C,
pub(super) _phantom: PhantomData<T>,
}
impl<T, C> Curve<T> for PingPongCurve<T, C>
where
C: Curve<T>,
{
#[inline]
fn domain(&self) -> Interval {
// This unwrap always succeeds because `curve` has a valid Interval as its domain and the
// length of `curve` cannot be NAN. It's still fine if it's infinity.
Interval::new(
self.curve.domain().start(),
self.curve.domain().end() + self.curve.domain().length(),
)
.unwrap()
}
#[inline]
fn sample_unchecked(&self, t: f32) -> T {
// the domain is bounded by construction
let final_t = if t > self.curve.domain().end() {
self.curve.domain().end() * 2.0 - t
} else {
t
};
self.curve.sample_unchecked(final_t)
}
}
/// The curve that results from chaining two curves.
///
/// Additionally the transition of the samples is guaranteed to not make sudden jumps. This is
/// useful if you really just know about the shapes of your curves and don't want to deal with
/// stitching them together properly when it would just introduce useless complexity. It is
/// 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`].
///
/// # Domain
///
/// The first curve's domain must be right-finite and the second's must be left-finite to get a
/// valid [`ContinuationCurve`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct ContinuationCurve<T, C, D> {
pub(super) first: C,
pub(super) second: D,
// cache the offset in the curve directly to prevent triple sampling for every sample we make
pub(super) offset: T,
pub(super) _phantom: PhantomData<T>,
}
impl<T, C, D> Curve<T> for ContinuationCurve<T, C, D>
where
T: VectorSpace,
C: Curve<T>,
D: Curve<T>,
{
#[inline]
fn domain(&self) -> Interval {
// This unwrap always succeeds because `curve` has a valid Interval as its domain and the
// length of `curve` 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_unchecked(&self, t: f32) -> T {
if t > self.first.domain().end() {
self.second.sample_unchecked(
// `t - first.domain.end` computes the offset into the domain of the second.
t - self.first.domain().end() + self.second.domain().start(),
) + self.offset
} else {
self.first.sample_unchecked(t)
}
}
}

View File

@ -2,21 +2,20 @@
//! contains the [`Interval`] type, along with a selection of core data structures used to back
//! curves that are interpolated from samples.
pub mod adaptors;
pub mod cores;
pub mod interval;
use adaptors::*;
pub use interval::{interval, Interval};
use itertools::Itertools;
use crate::StableInterpolate;
use crate::{StableInterpolate, VectorSpace};
use core::{marker::PhantomData, ops::Deref};
use cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError};
use interval::InvalidIntervalError;
use thiserror::Error;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
/// A trait for a type that can represent values of type `T` parametrized over a fixed interval.
///
/// Typical examples of this are actual geometric curves where `T: VectorSpace`, but other kinds
@ -136,7 +135,7 @@ pub trait Curve<T> {
/// # use bevy_math::vec2;
/// let my_curve = constant_curve(Interval::UNIT, 1.0);
/// let domain = my_curve.domain();
/// let reversed_curve = my_curve.reparametrize(domain, |t| domain.end() - t);
/// let reversed_curve = my_curve.reparametrize(domain, |t| domain.end() - (t - domain.start()));
///
/// // Take a segment of a curve:
/// # let my_curve = constant_curve(Interval::UNIT, 1.0);
@ -243,10 +242,14 @@ pub trait Curve<T> {
})
}
/// Create a new [`Curve`] by composing this curve end-to-end with another, producing another curve
/// Create a new [`Curve`] by composing this curve end-to-start with another, producing another curve
/// with outputs of the same type. The domain of the other curve is translated so that its start
/// coincides with where this curve ends. A [`ChainError`] is returned if this curve's domain
/// doesn't have a finite end or if `other`'s domain doesn't have a finite start.
/// coincides with where this curve ends.
///
/// # 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<C>(self, other: C) -> Result<ChainCurve<T, Self, C>, ChainError>
where
Self: Sized,
@ -265,6 +268,149 @@ pub trait Curve<T> {
})
}
/// Create a new [`Curve`] inverting this curve on the x-axis, producing another curve with
/// outputs of the same type, effectively playing backwards starting at `self.domain().end()`
/// and transitioning over to `self.domain().start()`. The domain of the new curve is still the
/// same.
///
/// # Error
///
/// A [`ReverseError`] is returned if this curve's domain isn't bounded.
fn reverse(self) -> Result<ReverseCurve<T, Self>, ReverseError>
where
Self: Sized,
{
self.domain()
.is_bounded()
.then(|| ReverseCurve {
curve: self,
_phantom: PhantomData,
})
.ok_or(ReverseError::SourceDomainEndInfinite)
}
/// Create a new [`Curve`] repeating this curve `N` times, producing another curve with outputs
/// of the same type. The domain of the new curve will be bigger by a factor of `n + 1`.
///
/// # Notes
///
/// - this doesn't guarantee a smooth transition from one occurrence of the curve to its next
/// iteration. The curve will make a jump if `self.domain().start() != self.domain().end()`!
/// - for `count == 0` the output of this adaptor is basically identical to the previous curve
/// - 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
///
/// 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,
{
self.domain()
.is_bounded()
.then(|| {
// This unwrap always succeeds because `curve` has a valid Interval as its domain and the
// length of `curve` cannot be NAN. It's still fine if it's infinity.
let domain = Interval::new(
self.domain().start(),
self.domain().end() + self.domain().length() * count as f32,
)
.unwrap();
RepeatCurve {
domain,
curve: self,
_phantom: PhantomData,
}
})
.ok_or(RepeatError::SourceDomainUnbounded)
}
/// Create a new [`Curve`] repeating this curve forever, producing another curve with
/// outputs of the same type. The domain of the new curve will be unbounded.
///
/// # Notes
///
/// - this doesn't guarantee a smooth transition from one occurrence of the curve to its next
/// iteration. The curve will make a jump if `self.domain().start() != self.domain().end()`!
/// - 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
///
/// A [`RepeatError`] is returned if this curve's domain isn't bounded.
fn forever(self) -> Result<ForeverCurve<T, Self>, RepeatError>
where
Self: Sized,
{
self.domain()
.is_bounded()
.then(|| ForeverCurve {
curve: self,
_phantom: PhantomData,
})
.ok_or(RepeatError::SourceDomainUnbounded)
}
/// Create a new [`Curve`] chaining the original curve with its inverse, producing
/// 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
///
/// 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,
{
self.domain()
.has_finite_end()
.then(|| PingPongCurve {
curve: self,
_phantom: PhantomData,
})
.ok_or(PingPongError::SourceDomainEndInfinite)
}
/// Create a new [`Curve`] by composing this curve end-to-start 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.
///
///
/// Additionally the transition of the samples is guaranteed to make no sudden jumps. This is
/// useful if you really just know about the shapes of your curves and don't want to deal with
/// stitching them together properly when it would just introduce useless complexity. It is
/// realized by translating the other curve so that its start sample point coincides with the
/// current curves' end sample point.
///
/// # Error
///
/// 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>,
{
if !self.domain().has_finite_end() {
return Err(ChainError::FirstEndInfinite);
}
if !other.domain().has_finite_start() {
return Err(ChainError::SecondStartInfinite);
}
let offset = self.sample_unchecked(self.domain().end())
- other.sample_unchecked(self.domain().start());
Ok(ContinuationCurve {
first: self,
second: other,
offset,
_phantom: PhantomData,
})
}
/// 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,
@ -483,6 +629,36 @@ pub enum LinearReparamError {
TargetIntervalUnbounded,
}
/// An error indicating that a reversion of a curve couldn't be performed because of
/// malformed inputs.
#[derive(Debug, Error)]
#[error("Could not reverse this curve")]
pub enum ReverseError {
/// The source curve that was to be reversed had unbounded domain end.
#[error("This curve has an unbounded domain end")]
SourceDomainEndInfinite,
}
/// An error indicating that a repetition of a curve couldn't be performed because of malformed
/// inputs.
#[derive(Debug, Error)]
#[error("Could not repeat this curve")]
pub enum RepeatError {
/// The source curve that was to be repeated had unbounded domain.
#[error("This curve has an unbounded domain")]
SourceDomainUnbounded,
}
/// An error indicating that a ping ponging of a curve couldn't be performed because of
/// malformed inputs.
#[derive(Debug, Error)]
#[error("Could not ping pong this curve")]
pub enum PingPongError {
/// The source curve that was to be ping ponged had unbounded domain end.
#[error("This curve has an unbounded domain end")]
SourceDomainEndInfinite,
}
/// An error indicating that an end-to-end composition couldn't be performed because of
/// malformed inputs.
#[derive(Debug, Error)]
@ -516,7 +692,7 @@ pub enum ResamplingError {
/// This is a curve that holds an inner value and always produces a clone of that value when sampled.
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct ConstantCurve<T> {
domain: Interval,
value: T,
@ -554,7 +730,7 @@ where
/// output of type `T`. The value of this curve when sampled at time `t` is just `f(t)`.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct FunctionCurve<T, F> {
domain: Interval,
f: F,
@ -595,7 +771,7 @@ where
/// given function. Curves of this type are produced by [`Curve::map`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct MapCurve<S, T, C, F> {
preimage: C,
f: F,
@ -622,7 +798,7 @@ where
/// Curves of this type are produced by [`Curve::reparametrize`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct ReparamCurve<T, C, F> {
domain: Interval,
base: C,
@ -650,9 +826,9 @@ where
/// Curves of this type are produced by [`Curve::reparametrize_linear`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct LinearReparamCurve<T, C> {
/// Invariants: The domain of this curve must always be bounded.
/// Invariants: The domain of the inner curve must always be bounded.
base: C,
/// Invariants: This interval must always be bounded.
new_domain: Interval,
@ -680,7 +856,7 @@ where
/// sample times before sampling. Curves of this type are produced by [`Curve::reparametrize_by_curve`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct CurveReparamCurve<T, C, D> {
base: C,
reparam_curve: D,
@ -708,7 +884,7 @@ where
/// produced by [`Curve::graph`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct GraphCurve<T, C> {
base: C,
_phantom: PhantomData<T>,
@ -733,7 +909,7 @@ where
/// of this type are produced by [`Curve::zip`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct ProductCurve<S, T, C, D> {
domain: Interval,
first: C,
@ -760,55 +936,10 @@ where
}
}
/// The curve that results from chaining one curve with another. The second curve is
/// effectively reparametrized so that its start is at the end of the first.
///
/// For this to be well-formed, the first curve's domain must be right-finite and the second's
/// must be left-finite.
///
/// Curves of this type are produced by [`Curve::chain`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
pub struct ChainCurve<T, C, D> {
first: C,
second: D,
_phantom: PhantomData<T>,
}
impl<T, C, D> Curve<T> for ChainCurve<T, C, D>
where
C: Curve<T>,
D: Curve<T>,
{
#[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_unchecked(&self, t: f32) -> T {
if t > self.first.domain().end() {
self.second.sample_unchecked(
// `t - first.domain.end` computes the offset into the domain of the second.
t - self.first.domain().end() + self.second.domain().start(),
)
} else {
self.first.sample_unchecked(t)
}
}
}
/// A curve that is defined by explicit neighbor interpolation over a set of samples.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct SampleCurve<T, I> {
core: EvenCore<T>,
interpolation: I,
@ -856,7 +987,7 @@ impl<T, I> SampleCurve<T, I> {
/// 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))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct SampleAutoCurve<T> {
core: EvenCore<T>,
}
@ -895,7 +1026,7 @@ impl<T> SampleAutoCurve<T> {
/// interpolation.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct UnevenSampleCurve<T, I> {
core: UnevenCore<T>,
interpolation: I,
@ -953,7 +1084,7 @@ impl<T, I> UnevenSampleCurve<T, I> {
/// 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))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct UnevenSampleAutoCurve<T> {
core: UnevenCore<T>,
}
@ -1072,6 +1203,91 @@ mod tests {
assert_eq!(mapped_curve.domain(), Interval::UNIT);
}
#[test]
fn reverse() {
let curve = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
let rev_curve = curve.reverse().unwrap();
assert_eq!(rev_curve.sample(-0.1), None);
assert_eq!(rev_curve.sample(0.0), Some(1.0 * 3.0 + 1.0));
assert_eq!(rev_curve.sample(0.5), Some(0.5 * 3.0 + 1.0));
assert_eq!(rev_curve.sample(1.0), Some(0.0 * 3.0 + 1.0));
assert_eq!(rev_curve.sample(1.1), None);
let curve = function_curve(Interval::new(-2.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
let rev_curve = curve.reverse().unwrap();
assert_eq!(rev_curve.sample(-2.1), None);
assert_eq!(rev_curve.sample(-2.0), Some(1.0 * 3.0 + 1.0));
assert_eq!(rev_curve.sample(-0.5), Some(-0.5 * 3.0 + 1.0));
assert_eq!(rev_curve.sample(1.0), Some(-2.0 * 3.0 + 1.0));
assert_eq!(rev_curve.sample(1.1), None);
}
#[test]
fn repeat() {
let curve = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
let repeat_curve = curve.by_ref().repeat(1).unwrap();
assert_eq!(repeat_curve.sample(-0.1), None);
assert_eq!(repeat_curve.sample(0.0), Some(0.0 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(0.5), Some(0.5 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(0.99), Some(0.99 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(1.0), Some(1.0 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(1.01), Some(0.01 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(1.5), Some(0.5 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(1.99), Some(0.99 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(2.0), Some(1.0 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(2.01), None);
let repeat_curve = curve.by_ref().repeat(3).unwrap();
assert_eq!(repeat_curve.sample(2.0), Some(1.0 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(3.0), Some(1.0 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(4.0), Some(1.0 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(5.0), None);
let repeat_curve = curve.by_ref().forever().unwrap();
assert_eq!(repeat_curve.sample(-1.0), Some(1.0 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(2.0), Some(1.0 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(3.0), Some(1.0 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(4.0), Some(1.0 * 3.0 + 1.0));
assert_eq!(repeat_curve.sample(5.0), Some(1.0 * 3.0 + 1.0));
}
#[test]
fn ping_pong() {
let curve = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
let ping_pong_curve = curve.ping_pong().unwrap();
assert_eq!(ping_pong_curve.sample(-0.1), None);
assert_eq!(ping_pong_curve.sample(0.0), Some(0.0 * 3.0 + 1.0));
assert_eq!(ping_pong_curve.sample(0.5), Some(0.5 * 3.0 + 1.0));
assert_eq!(ping_pong_curve.sample(1.0), Some(1.0 * 3.0 + 1.0));
assert_eq!(ping_pong_curve.sample(1.5), Some(0.5 * 3.0 + 1.0));
assert_eq!(ping_pong_curve.sample(2.0), Some(0.0 * 3.0 + 1.0));
assert_eq!(ping_pong_curve.sample(2.1), None);
let curve = function_curve(Interval::new(-2.0, 2.0).unwrap(), |t| t * 3.0 + 1.0);
let ping_pong_curve = curve.ping_pong().unwrap();
assert_eq!(ping_pong_curve.sample(-2.1), None);
assert_eq!(ping_pong_curve.sample(-2.0), Some(-2.0 * 3.0 + 1.0));
assert_eq!(ping_pong_curve.sample(-0.5), Some(-0.5 * 3.0 + 1.0));
assert_eq!(ping_pong_curve.sample(2.0), Some(2.0 * 3.0 + 1.0));
assert_eq!(ping_pong_curve.sample(4.5), Some(-0.5 * 3.0 + 1.0));
assert_eq!(ping_pong_curve.sample(6.0), Some(-2.0 * 3.0 + 1.0));
assert_eq!(ping_pong_curve.sample(6.1), None);
}
#[test]
fn continue_chain() {
let first = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
let second = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * t);
let c0_chain_curve = first.chain_continue(second).unwrap();
assert_eq!(c0_chain_curve.sample(-0.1), None);
assert_eq!(c0_chain_curve.sample(0.0), Some(0.0 * 3.0 + 1.0));
assert_eq!(c0_chain_curve.sample(0.5), Some(0.5 * 3.0 + 1.0));
assert_eq!(c0_chain_curve.sample(1.0), Some(1.0 * 3.0 + 1.0));
assert_eq!(c0_chain_curve.sample(1.5), Some(1.0 * 3.0 + 1.0 + 0.25));
assert_eq!(c0_chain_curve.sample(2.0), Some(1.0 * 3.0 + 1.0 + 1.0));
assert_eq!(c0_chain_curve.sample(2.1), None);
}
#[test]
fn reparametrization() {
let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), ops::log2);