bevy/crates/bevy_math/src/curve/adaptors.rs
Matty Weatherley c60dcea231
Derivative access patterns for curves (#16503)
# Objective

- For curves that also include derivatives, make accessing derivative
information via the `Curve` API ergonomic: that is, provide access to a
curve that also samples derivative information.
- Implement this functionality for cubic spline curves provided by
`bevy_math`.

Ultimately, this is to serve the purpose of doing more geometric
operations on curves, like reparametrization by arclength and the
construction of moving frames.

## Solution

This has several parts, some of which may seem redundant. However, care
has been put into this to satisfy the following constraints:
- Accessing a `Curve` that samples derivative information should be not
just possible but easy and non-error-prone. For example, given a
differentiable `Curve<Vec2>`, one should be able to access something
like a `Curve<(Vec2, Vec2)>` ergonomically, and not just sample the
derivatives piecemeal from point to point.
- Derivative access should not step on the toes of ordinary curve usage.
In particular, in the above scenario, we want to avoid simply making the
same curve both a `Curve<Vec2>` and a `Curve<(Vec2, Vec2)>` because this
requires manual disambiguation when the API is used.
- Derivative access must work gracefully in both owned and borrowed
contexts.

### `HasTangent`

We introduce a trait `HasTangent` that provides an associated `Tangent`
type for types that have tangent spaces:
```rust
pub trait HasTangent {
    /// The tangent type.
    type Tangent: VectorSpace;
}
```

(Mathematically speaking, it would be more precise to say that these are
types that represent spaces which are canonically
[parallelized](https://en.wikipedia.org/wiki/Parallelizable_manifold). )

The idea here is that a point moving through a `HasTangent` type may
have a derivative valued in the associated `Tangent` type at each time
in its journey. We reify this with a `WithDerivative<T>` type that uses
`HasTangent` to include derivative information:
```rust
pub struct WithDerivative<T>
where
    T: HasTangent,
{
    /// The underlying value.
    pub value: T,

    /// The derivative at `value`.
    pub derivative: T::Tangent,
}
```

And we can play the same game with second derivatives as well, since
every `VectorSpace` type is `HasTangent` where `Tangent` is itself (we
may want to be more restrictive with this in practice, but this holds
mathematically).
```rust
pub struct WithTwoDerivatives<T>
where
    T: HasTangent,
{
    /// The underlying value.
    pub value: T,

    /// The derivative at `value`.
    pub derivative: T::Tangent,

    /// The second derivative at `value`.
    pub second_derivative: <T::Tangent as HasTangent>::Tangent,
}
```

In this PR, `HasTangent` is only implemented for `VectorSpace` types,
but it would be valuable to have this implementation for types like
`Rot2` and `Quat` as well. We could also do it for the isometry types
and, potentially, transforms as well. (This is in decreasing order of
value in my opinion.)

### `CurveWithDerivative`

This is a trait for a `Curve<T>` which allows the construction of a
`Curve<WithDerivative<T>>` when derivative information is known
intrinsically. It looks like this:
```rust
/// Trait for curves that have a well-defined notion of derivative, allowing for
/// derivatives to be extracted along with values.
pub trait CurveWithDerivative<T>
where
    T: HasTangent,
{
    /// This curve, but with its first derivative included in sampling.
    fn with_derivative(self) -> impl Curve<WithDerivative<T>>;
}
```

The idea here is to provide patterns like this:
```rust
let value_and_derivative = my_curve.with_derivative().sample_clamped(t);
```

One of the main points here is that `Curve<WithDerivative<T>>` is useful
as an output because it can be used durably. For example, in a dynamic
context, something that needs curves with derivatives can store
something like a `Box<dyn Curve<WithDerivative<T>>>`. Note that
`CurveWithDerivative` is not dyn-compatible.

### `SampleDerivative`

Many curves "know" how to sample their derivatives instrinsically, but
implementing `CurveWithDerivative` as given would be onerous or require
an annoying amount of boilerplate. There are also hurdles to overcome
that involve references to curves: for the `Curve` API, the expectation
is that curve transformations like `with_derivative` take things by
value, with the contract that they can still be used by reference
through deref-magic by including `by_ref` in a method chain.

These problems are solved simultaneously by a trait `SampleDerivative`
which, when implemented, automatically derives `CurveWithDerivative` for
a type and all types that dereference to it. It just looks like this:
```rust
pub trait SampleDerivative<T>: Curve<T>
where
    T: HasTangent,
{
    fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T>;
    // ... other sampling variants as default methods
}
```

The point is that the output of `with_derivative` is a
`Curve<WithDerivative<T>>` that uses the `SampleDerivative`
implementation. On a `SampleDerivative` type, you can also just call
`my_curve.sample_with_derivative(t)` instead of something like
`my_curve.by_ref().with_derivative().sample(t)`, which is more verbose
and less accessible.

In practice, `CurveWithDerivative<T>` is actually a "sealed" extension
trait of `SampleDerivative<T>`.

## Adaptors

`SampleDerivative` has automatic implementations on all curve adaptors
except for `FunctionCurve`, `MapCurve`, and `ReparamCurve` (because we
do not have a notion of differentiable Rust functions).

For example, `CurveReparamCurve` (the reparametrization of a curve by
another curve) can compute derivatives using the chain rule in the case
both its constituents have them.

## Testing

Tests for derivatives on the curve adaptors are included.

---

## Showcase

This development allows derivative information to be included with and
extracted from curves using the `Curve` API.
```rust
let points = [
    vec2(-1.0, -20.0),
    vec2(3.0, 2.0),
    vec2(5.0, 3.0),
    vec2(9.0, 8.0),
];

// A cubic spline curve that goes through `points`.
let curve = CubicCardinalSpline::new(0.3, points).to_curve().unwrap();

// Calling `with_derivative` causes derivative output to be included in the output of the curve API.
let curve_with_derivative = curve.with_derivative();

// A `Curve<f32>` that outputs the speed of the original.
let speed_curve = curve_with_derivative.map(|x| x.derivative.norm());
```

---

## Questions

- ~~Maybe we should seal `WithDerivative` or make it require
`SampleDerivative` (i.e. make it unimplementable except through
`SampleDerivative`).~~ I decided this is a good idea.
- ~~Unclear whether `VectorSpace: HasTangent` blanket implementation is
really appropriate. For colors, for example, I'm not sure that the
derivative values can really be interpreted as a color. In any case, it
should still remain the case that `VectorSpace` types are `HasTangent`
and that `HasTangent::Tangent: HasTangent`.~~ I think this is fine.
- Infinity bikeshed on names of traits and things.

## Future

- Faster implementations of `SampleDerivative` for cubic spline curves.
- Improve ergonomics for accessing only derivatives (and other kinds of
transformations on derivative curves).
- Implement `HasTangent` for:
  - `Rot2`/`Quat`
  - `Isometry` types
  - `Transform`, maybe
- Implement derivatives for easing curves.
- Marker traits for continuous/differentiable curves. (It's actually
unclear to me how much value this has in practice, but we have discussed
it in the past.)

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-12-10 20:27:37 +00:00

810 lines
23 KiB
Rust

//! Adaptors used by the Curve API for transforming and combining curves together.
use super::interval::*;
use super::Curve;
use crate::ops;
use crate::VectorSpace;
use core::any::type_name;
use core::fmt::{self, Debug};
use core::marker::PhantomData;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::{utility::GenericTypePathCell, FromReflect, Reflect, TypePath};
#[cfg(feature = "bevy_reflect")]
mod paths {
pub(super) const THIS_MODULE: &str = "bevy_math::curve::adaptors";
pub(super) const THIS_CRATE: &str = "bevy_math";
}
// NOTE ON REFLECTION:
//
// Function members of structs pose an obstacle for reflection, because they don't implement
// reflection traits themselves. Some of these are more problematic than others; for example,
// `FromReflect` is basically hopeless for function members regardless, so function-containing
// adaptors will just never be `FromReflect` (at least until function item types implement
// Default, if that ever happens). Similarly, they do not implement `TypePath`, and as a result,
// those adaptors also need custom `TypePath` adaptors which use `type_name` instead.
//
// The sum total weirdness of the `Reflect` implementations amounts to this; those adaptors:
// - are currently never `FromReflect`;
// - have custom `TypePath` implementations which are not fully stable;
// - have custom `Debug` implementations which display the function only by type name.
/// A curve with a constant value over its domain.
///
/// This is a curve that holds an inner value and always produces a clone of that value when sampled.
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
pub struct ConstantCurve<T> {
pub(crate) domain: Interval,
pub(crate) value: T,
}
impl<T> ConstantCurve<T>
where
T: Clone,
{
/// Create a constant curve, which has the given `domain` and always produces the given `value`
/// when sampled.
pub fn new(domain: Interval, value: T) -> Self {
Self { domain, value }
}
}
impl<T> Curve<T> for ConstantCurve<T>
where
T: Clone,
{
#[inline]
fn domain(&self) -> Interval {
self.domain
}
#[inline]
fn sample_unchecked(&self, _t: f32) -> T {
self.value.clone()
}
}
/// A curve defined by a function together with a fixed domain.
///
/// This is a curve that holds an inner function `f` which takes numbers (`f32`) as input and produces
/// output of type `T`. The value of this curve when sampled at time `t` is just `f(t)`.
#[derive(Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(where T: TypePath),
reflect(from_reflect = false, type_path = false),
)]
pub struct FunctionCurve<T, F> {
pub(crate) domain: Interval,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) f: F,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) _phantom: PhantomData<fn() -> T>,
}
impl<T, F> Debug for FunctionCurve<T, F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FunctionCurve")
.field("domain", &self.domain)
.field("f", &type_name::<F>())
.finish()
}
}
/// Note: This is not a fully stable implementation of `TypePath` due to usage of `type_name`
/// for function members.
#[cfg(feature = "bevy_reflect")]
impl<T, F> TypePath for FunctionCurve<T, F>
where
T: TypePath,
F: 'static,
{
fn type_path() -> &'static str {
static CELL: GenericTypePathCell = GenericTypePathCell::new();
CELL.get_or_insert::<Self, _>(|| {
format!(
"{}::FunctionCurve<{},{}>",
paths::THIS_MODULE,
T::type_path(),
type_name::<F>()
)
})
}
fn short_type_path() -> &'static str {
static CELL: GenericTypePathCell = GenericTypePathCell::new();
CELL.get_or_insert::<Self, _>(|| {
format!(
"FunctionCurve<{},{}>",
T::short_type_path(),
type_name::<F>()
)
})
}
fn type_ident() -> Option<&'static str> {
Some("FunctionCurve")
}
fn crate_name() -> Option<&'static str> {
Some(paths::THIS_CRATE)
}
fn module_path() -> Option<&'static str> {
Some(paths::THIS_MODULE)
}
}
impl<T, F> FunctionCurve<T, F>
where
F: Fn(f32) -> T,
{
/// Create a new curve with the given `domain` from the given `function`. When sampled, the
/// `function` is evaluated at the sample time to compute the output.
pub fn new(domain: Interval, function: F) -> Self {
FunctionCurve {
domain,
f: function,
_phantom: PhantomData,
}
}
}
impl<T, F> Curve<T> for FunctionCurve<T, F>
where
F: Fn(f32) -> T,
{
#[inline]
fn domain(&self) -> Interval {
self.domain
}
#[inline]
fn sample_unchecked(&self, t: f32) -> T {
(self.f)(t)
}
}
/// 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`].
#[derive(Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(where S: TypePath, T: TypePath, C: TypePath),
reflect(from_reflect = false, type_path = false),
)]
pub struct MapCurve<S, T, C, F> {
pub(crate) preimage: C,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) f: F,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) _phantom: PhantomData<(fn() -> S, fn(S) -> T)>,
}
impl<S, T, C, F> Debug for MapCurve<S, T, C, F>
where
C: Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MapCurve")
.field("preimage", &self.preimage)
.field("f", &type_name::<F>())
.finish()
}
}
/// Note: This is not a fully stable implementation of `TypePath` due to usage of `type_name`
/// for function members.
#[cfg(feature = "bevy_reflect")]
impl<S, T, C, F> TypePath for MapCurve<S, T, C, F>
where
S: TypePath,
T: TypePath,
C: TypePath,
F: 'static,
{
fn type_path() -> &'static str {
static CELL: GenericTypePathCell = GenericTypePathCell::new();
CELL.get_or_insert::<Self, _>(|| {
format!(
"{}::MapCurve<{},{},{},{}>",
paths::THIS_MODULE,
S::type_path(),
T::type_path(),
C::type_path(),
type_name::<F>()
)
})
}
fn short_type_path() -> &'static str {
static CELL: GenericTypePathCell = GenericTypePathCell::new();
CELL.get_or_insert::<Self, _>(|| {
format!(
"MapCurve<{},{},{},{}>",
S::type_path(),
T::type_path(),
C::type_path(),
type_name::<F>()
)
})
}
fn type_ident() -> Option<&'static str> {
Some("MapCurve")
}
fn crate_name() -> Option<&'static str> {
Some(paths::THIS_CRATE)
}
fn module_path() -> Option<&'static str> {
Some(paths::THIS_MODULE)
}
}
impl<S, T, C, F> Curve<T> for MapCurve<S, T, C, F>
where
C: Curve<S>,
F: Fn(S) -> T,
{
#[inline]
fn domain(&self) -> Interval {
self.preimage.domain()
}
#[inline]
fn sample_unchecked(&self, t: f32) -> T {
(self.f)(self.preimage.sample_unchecked(t))
}
}
/// A curve whose sample space is mapped onto that of some base curve's before sampling.
/// Curves of this type are produced by [`Curve::reparametrize`].
#[derive(Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(where T: TypePath, C: TypePath),
reflect(from_reflect = false, type_path = false),
)]
pub struct ReparamCurve<T, C, F> {
pub(crate) domain: Interval,
pub(crate) base: C,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) f: F,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) _phantom: PhantomData<fn() -> T>,
}
impl<T, C, F> Debug for ReparamCurve<T, C, F>
where
C: Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ReparamCurve")
.field("domain", &self.domain)
.field("base", &self.base)
.field("f", &type_name::<F>())
.finish()
}
}
/// Note: This is not a fully stable implementation of `TypePath` due to usage of `type_name`
/// for function members.
#[cfg(feature = "bevy_reflect")]
impl<T, C, F> TypePath for ReparamCurve<T, C, F>
where
T: TypePath,
C: TypePath,
F: 'static,
{
fn type_path() -> &'static str {
static CELL: GenericTypePathCell = GenericTypePathCell::new();
CELL.get_or_insert::<Self, _>(|| {
format!(
"{}::ReparamCurve<{},{},{}>",
paths::THIS_MODULE,
T::type_path(),
C::type_path(),
type_name::<F>()
)
})
}
fn short_type_path() -> &'static str {
static CELL: GenericTypePathCell = GenericTypePathCell::new();
CELL.get_or_insert::<Self, _>(|| {
format!(
"ReparamCurve<{},{},{}>",
T::type_path(),
C::type_path(),
type_name::<F>()
)
})
}
fn type_ident() -> Option<&'static str> {
Some("ReparamCurve")
}
fn crate_name() -> Option<&'static str> {
Some(paths::THIS_CRATE)
}
fn module_path() -> Option<&'static str> {
Some(paths::THIS_MODULE)
}
}
impl<T, C, F> Curve<T> for ReparamCurve<T, C, F>
where
C: Curve<T>,
F: Fn(f32) -> f32,
{
#[inline]
fn domain(&self) -> Interval {
self.domain
}
#[inline]
fn sample_unchecked(&self, t: f32) -> T {
self.base.sample_unchecked((self.f)(t))
}
}
/// 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`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect, FromReflect),
reflect(from_reflect = false)
)]
pub struct LinearReparamCurve<T, C> {
/// Invariants: The domain of this curve must always be bounded.
pub(crate) base: C,
/// Invariants: This interval must always be bounded.
pub(crate) new_domain: Interval,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) _phantom: PhantomData<fn() -> T>,
}
impl<T, C> Curve<T> for LinearReparamCurve<T, C>
where
C: Curve<T>,
{
#[inline]
fn domain(&self) -> Interval {
self.new_domain
}
#[inline]
fn sample_unchecked(&self, t: f32) -> T {
// The invariants imply this unwrap always succeeds.
let f = self.new_domain.linear_map_to(self.base.domain()).unwrap();
self.base.sample_unchecked(f(t))
}
}
/// 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`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect, FromReflect),
reflect(from_reflect = false)
)]
pub struct CurveReparamCurve<T, C, D> {
pub(crate) base: C,
pub(crate) reparam_curve: D,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) _phantom: PhantomData<fn() -> T>,
}
impl<T, C, D> Curve<T> for CurveReparamCurve<T, C, D>
where
C: Curve<T>,
D: Curve<f32>,
{
#[inline]
fn domain(&self) -> Interval {
self.reparam_curve.domain()
}
#[inline]
fn sample_unchecked(&self, t: f32) -> T {
let sample_time = self.reparam_curve.sample_unchecked(t);
self.base.sample_unchecked(sample_time)
}
}
/// A curve that is the graph of another curve over its parameter space. Curves of this type are
/// produced by [`Curve::graph`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect, FromReflect),
reflect(from_reflect = false)
)]
pub struct GraphCurve<T, C> {
pub(crate) base: C,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) _phantom: PhantomData<fn() -> T>,
}
impl<T, C> Curve<(f32, T)> for GraphCurve<T, C>
where
C: Curve<T>,
{
#[inline]
fn domain(&self) -> Interval {
self.base.domain()
}
#[inline]
fn sample_unchecked(&self, t: f32) -> (f32, T) {
(t, self.base.sample_unchecked(t))
}
}
/// A curve that combines the output data from two constituent curves into a tuple output. Curves
/// of this type are produced by [`Curve::zip`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect, FromReflect),
reflect(from_reflect = false)
)]
pub struct ZipCurve<S, T, C, D> {
pub(crate) domain: Interval,
pub(crate) first: C,
pub(crate) second: D,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) _phantom: PhantomData<fn() -> (S, T)>,
}
impl<S, T, C, D> Curve<(S, T)> for ZipCurve<S, T, C, D>
where
C: Curve<S>,
D: Curve<T>,
{
#[inline]
fn domain(&self) -> Interval {
self.domain
}
#[inline]
fn sample_unchecked(&self, t: f32) -> (S, T) {
(
self.first.sample_unchecked(t),
self.second.sample_unchecked(t),
)
}
}
/// The curve that results from chaining one curve with another. The second curve is
/// effectively reparametrized so that its start is at the end of the first.
///
/// For this to be well-formed, the first curve's domain must be right-finite and the second's
/// must be left-finite.
///
/// 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, FromReflect),
reflect(from_reflect = false)
)]
pub struct ChainCurve<T, C, D> {
pub(crate) first: C,
pub(crate) second: D,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) _phantom: PhantomData<fn() -> 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(Reflect, FromReflect),
reflect(from_reflect = false)
)]
pub struct ReverseCurve<T, C> {
pub(crate) curve: C,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) _phantom: PhantomData<fn() -> 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(Reflect, FromReflect),
reflect(from_reflect = false)
)]
pub struct RepeatCurve<T, C> {
pub(crate) domain: Interval,
pub(crate) curve: C,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) _phantom: PhantomData<fn() -> 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 {
let t = self.base_curve_sample_time(t);
self.curve.sample_unchecked(t)
}
}
impl<T, C> RepeatCurve<T, C>
where
C: Curve<T>,
{
#[inline]
pub(crate) fn base_curve_sample_time(&self, t: f32) -> f32 {
// the domain is bounded by construction
let d = self.curve.domain();
let cyclic_t = ops::rem_euclid(t - d.start(), d.length());
if t != d.start() && cyclic_t == 0.0 {
d.end()
} else {
d.start() + cyclic_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(Reflect, FromReflect),
reflect(from_reflect = false)
)]
pub struct ForeverCurve<T, C> {
pub(crate) curve: C,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) _phantom: PhantomData<fn() -> 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 {
let t = self.base_curve_sample_time(t);
self.curve.sample_unchecked(t)
}
}
impl<T, C> ForeverCurve<T, C>
where
C: Curve<T>,
{
#[inline]
pub(crate) fn base_curve_sample_time(&self, t: f32) -> f32 {
// the domain is bounded by construction
let d = self.curve.domain();
let cyclic_t = ops::rem_euclid(t - d.start(), d.length());
if t != d.start() && cyclic_t == 0.0 {
d.end()
} else {
d.start() + cyclic_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(Reflect, FromReflect),
reflect(from_reflect = false)
)]
pub struct PingPongCurve<T, C> {
pub(crate) curve: C,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) _phantom: PhantomData<fn() -> 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(Reflect, FromReflect),
reflect(from_reflect = false)
)]
pub struct ContinuationCurve<T, C, D> {
pub(crate) first: C,
pub(crate) second: D,
// cache the offset in the curve directly to prevent triple sampling for every sample we make
pub(crate) offset: T,
#[cfg_attr(feature = "bevy_reflect", reflect(ignore))]
pub(crate) _phantom: PhantomData<fn() -> 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)
}
}
}