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>
This commit is contained in:
Matty Weatherley 2024-12-10 15:27:37 -05:00 committed by GitHub
parent 711246aa34
commit c60dcea231
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1243 additions and 96 deletions

View file

@ -30,7 +30,7 @@ pub trait VectorSpace:
+ Div<f32, Output = Self>
+ Add<Self, Output = Self>
+ Sub<Self, Output = Self>
+ Neg
+ Neg<Output = Self>
+ Default
+ Debug
+ Clone
@ -71,6 +71,89 @@ impl VectorSpace for f32 {
const ZERO: Self = 0.0;
}
/// A type consisting of formal sums of elements from `V` and `W`. That is,
/// each value `Sum(v, w)` is thought of as `v + w`, with no available
/// simplification. In particular, if `V` and `W` are [vector spaces], then
/// `Sum<V, W>` is a vector space whose dimension is the sum of those of `V`
/// and `W`, and the field accessors `.0` and `.1` are vector space projections.
///
/// [vector spaces]: VectorSpace
#[derive(Debug, Clone, Copy)]
pub struct Sum<V, W>(pub V, pub W);
impl<V, W> Mul<f32> for Sum<V, W>
where
V: VectorSpace,
W: VectorSpace,
{
type Output = Self;
fn mul(self, rhs: f32) -> Self::Output {
Sum(self.0 * rhs, self.1 * rhs)
}
}
impl<V, W> Div<f32> for Sum<V, W>
where
V: VectorSpace,
W: VectorSpace,
{
type Output = Self;
fn div(self, rhs: f32) -> Self::Output {
Sum(self.0 / rhs, self.1 / rhs)
}
}
impl<V, W> Add<Self> for Sum<V, W>
where
V: VectorSpace,
W: VectorSpace,
{
type Output = Self;
fn add(self, other: Self) -> Self::Output {
Sum(self.0 + other.0, self.1 + other.1)
}
}
impl<V, W> Sub<Self> for Sum<V, W>
where
V: VectorSpace,
W: VectorSpace,
{
type Output = Self;
fn sub(self, other: Self) -> Self::Output {
Sum(self.0 - other.0, self.1 - other.1)
}
}
impl<V, W> Neg for Sum<V, W>
where
V: VectorSpace,
W: VectorSpace,
{
type Output = Self;
fn neg(self) -> Self::Output {
Sum(-self.0, -self.1)
}
}
impl<V, W> Default for Sum<V, W>
where
V: VectorSpace,
W: VectorSpace,
{
fn default() -> Self {
Sum(V::default(), W::default())
}
}
impl<V, W> VectorSpace for Sum<V, W>
where
V: VectorSpace,
W: VectorSpace,
{
const ZERO: Self = Sum(V::ZERO, W::ZERO);
}
/// A type that supports the operations of a normed vector space; i.e. a norm operation in addition
/// to those of [`VectorSpace`]. Specifically, the implementor must guarantee that the following
/// relationships hold, within the limitations of floating point arithmetic:
@ -410,3 +493,48 @@ impl_stable_interpolate_tuple!(
(T9, 9),
(T10, 10)
);
/// A type that has tangents.
pub trait HasTangent {
/// The tangent type.
type Tangent: VectorSpace;
}
/// A value with its derivative.
pub struct WithDerivative<T>
where
T: HasTangent,
{
/// The underlying value.
pub value: T,
/// The derivative at `value`.
pub derivative: T::Tangent,
}
/// A value together with its first and second derivatives.
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,
}
impl<V: VectorSpace> HasTangent for V {
type Tangent = V;
}
impl<M, N> HasTangent for (M, N)
where
M: HasTangent,
N: HasTangent,
{
type Tangent = Sum<M::Tangent, N::Tangent>;
}

View file

@ -0,0 +1,159 @@
use super::{CubicSegment, RationalSegment};
use crate::common_traits::{VectorSpace, WithDerivative, WithTwoDerivatives};
use crate::curve::{
derivatives::{SampleDerivative, SampleTwoDerivatives},
Curve, Interval,
};
#[cfg(feature = "alloc")]
use super::{CubicCurve, RationalCurve};
// -- CubicSegment
impl<P: VectorSpace> Curve<P> for CubicSegment<P> {
#[inline]
fn domain(&self) -> Interval {
Interval::UNIT
}
#[inline]
fn sample_unchecked(&self, t: f32) -> P {
self.position(t)
}
}
impl<P: VectorSpace> SampleDerivative<P> for CubicSegment<P> {
#[inline]
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<P> {
WithDerivative {
value: self.position(t),
derivative: self.velocity(t),
}
}
}
impl<P: VectorSpace> SampleTwoDerivatives<P> for CubicSegment<P> {
#[inline]
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<P> {
WithTwoDerivatives {
value: self.position(t),
derivative: self.velocity(t),
second_derivative: self.acceleration(t),
}
}
}
// -- CubicCurve
#[cfg(feature = "alloc")]
impl<P: VectorSpace> Curve<P> for CubicCurve<P> {
#[inline]
fn domain(&self) -> Interval {
// The non-emptiness invariant guarantees that this succeeds.
Interval::new(0.0, self.segments.len() as f32)
.expect("CubicCurve is invalid because it has no segments")
}
#[inline]
fn sample_unchecked(&self, t: f32) -> P {
self.position(t)
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> SampleDerivative<P> for CubicCurve<P> {
#[inline]
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<P> {
WithDerivative {
value: self.position(t),
derivative: self.velocity(t),
}
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> SampleTwoDerivatives<P> for CubicCurve<P> {
#[inline]
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<P> {
WithTwoDerivatives {
value: self.position(t),
derivative: self.velocity(t),
second_derivative: self.acceleration(t),
}
}
}
// -- RationalSegment
impl<P: VectorSpace> Curve<P> for RationalSegment<P> {
#[inline]
fn domain(&self) -> Interval {
Interval::UNIT
}
#[inline]
fn sample_unchecked(&self, t: f32) -> P {
self.position(t)
}
}
impl<P: VectorSpace> SampleDerivative<P> for RationalSegment<P> {
#[inline]
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<P> {
WithDerivative {
value: self.position(t),
derivative: self.velocity(t),
}
}
}
impl<P: VectorSpace> SampleTwoDerivatives<P> for RationalSegment<P> {
#[inline]
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<P> {
WithTwoDerivatives {
value: self.position(t),
derivative: self.velocity(t),
second_derivative: self.acceleration(t),
}
}
}
// -- RationalCurve
#[cfg(feature = "alloc")]
impl<P: VectorSpace> Curve<P> for RationalCurve<P> {
#[inline]
fn domain(&self) -> Interval {
// The non-emptiness invariant guarantees the success of this.
Interval::new(0.0, self.length())
.expect("RationalCurve is invalid because it has zero length")
}
#[inline]
fn sample_unchecked(&self, t: f32) -> P {
self.position(t)
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> SampleDerivative<P> for RationalCurve<P> {
#[inline]
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<P> {
WithDerivative {
value: self.position(t),
derivative: self.velocity(t),
}
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> SampleTwoDerivatives<P> for RationalCurve<P> {
#[inline]
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<P> {
WithTwoDerivatives {
value: self.position(t),
derivative: self.velocity(t),
second_derivative: self.acceleration(t),
}
}
}

View file

@ -1,22 +1,16 @@
//! Provides types for building cubic splines for rendering curves and use with animation easing.
use core::fmt::Debug;
#[cfg(feature = "curve")]
mod curve_impls;
use crate::{
ops::{self, FloatPow},
Vec2, VectorSpace,
};
use thiserror::Error;
#[cfg(feature = "alloc")]
use {alloc::vec, alloc::vec::Vec, core::iter::once, itertools::Itertools};
#[cfg(feature = "curve")]
use crate::curve::{Curve, Interval};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use thiserror::Error;
#[cfg(feature = "alloc")]
use {alloc::vec, alloc::vec::Vec, core::iter::once, itertools::Itertools};
/// A spline composed of a single cubic Bezier curve.
///
@ -24,15 +18,18 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
/// [`CubicSegment::new_bezier`] for use in easing.
///
/// ### Interpolation
///
/// The curve only passes through the first and last control point in each set of four points. The curve
/// is divided into "segments" by every fourth control point.
///
/// ### Tangency
///
/// Tangents are manually defined by the two intermediate control points within each set of four points.
/// You can think of the control points the curve passes through as "anchors", and as the intermediate
/// control points as the anchors displaced along their tangent vectors
///
/// ### Continuity
///
/// A Bezier curve is at minimum C0 continuous, meaning it has no holes or jumps. Each curve segment is
/// C2, meaning the tangent vector changes smoothly between each set of four control points, but this
/// doesn't hold at the control points between segments. Making the whole curve C1 or C2 requires moving
@ -52,8 +49,8 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
/// let bezier = CubicBezier::new(points).to_curve().unwrap();
/// let positions: Vec<_> = bezier.iter_positions(100).collect();
/// ```
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicBezier<P: VectorSpace> {
/// The control points of the Bezier curve.
@ -99,7 +96,6 @@ impl<P: VectorSpace> CubicGenerator<P> for CubicBezier<P> {
}
}
}
/// An error returned during cubic curve generation for cubic Bezier curves indicating that a
/// segment of control points was not present.
#[derive(Clone, Debug, Error)]
@ -114,16 +110,20 @@ pub struct CubicBezierError;
/// such as network prediction.
///
/// ### Interpolation
///
/// The curve passes through every control point.
///
/// ### Tangency
///
/// Tangents are explicitly defined at each control point.
///
/// ### Continuity
///
/// The curve is at minimum C1 continuous, meaning that it has no holes or jumps and the tangent vector also
/// has no sudden jumps.
///
/// ### Parametrization
///
/// The first segment of the curve connects the first two control points, the second connects the second and
/// third, and so on. This remains true when a cyclic curve is formed with [`to_curve_cyclic`], in which case
/// the final curve segment connects the last control point to the first.
@ -149,8 +149,8 @@ pub struct CubicBezierError;
/// ```
///
/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicHermite<P: VectorSpace> {
/// The control points of the Hermite curve.
@ -245,16 +245,20 @@ impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicHermite<P> {
/// **Note** the Catmull-Rom spline is a special case of Cardinal spline where the tension is 0.5.
///
/// ### Interpolation
///
/// The curve passes through every control point.
///
/// ### Tangency
///
/// Tangents are automatically computed based on the positions of control points.
///
/// ### Continuity
///
/// The curve is at minimum C1, meaning that it is continuous (it has no holes or jumps), and its tangent
/// vector is also well-defined everywhere, without sudden jumps.
///
/// ### Parametrization
///
/// The first segment of the curve connects the first two control points, the second connects the second and
/// third, and so on. This remains true when a cyclic curve is formed with [`to_curve_cyclic`], in which case
/// the final curve segment connects the last control point to the first.
@ -274,8 +278,8 @@ impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicHermite<P> {
/// ```
///
/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicCardinalSpline<P: VectorSpace> {
/// Tension
@ -404,17 +408,20 @@ impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicCardinalSpline<P> {
/// necessarily pass through any of the control points.
///
/// ### Interpolation
///
/// The curve does not necessarily pass through its control points.
///
/// ### Tangency
/// Tangents are automatically computed based on the positions of control points.
///
/// ### Continuity
///
/// The curve is C2 continuous, meaning it has no holes or jumps, the tangent vector changes smoothly along
/// the entire curve, and the acceleration also varies continuously. The acceleration continuity of this
/// spline makes it useful for camera paths.
///
/// ### Parametrization
///
/// Each curve segment is defined by a window of four control points taken in sequence. When [`to_curve_cyclic`]
/// is used to form a cyclic curve, the three additional segments used to close the curve come last.
///
@ -433,14 +440,13 @@ impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicCardinalSpline<P> {
/// ```
///
/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicBSpline<P: VectorSpace> {
/// The control points of the spline
pub control_points: Vec<P>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicBSpline<P> {
/// Build a new B-Spline.
@ -564,6 +570,7 @@ pub enum CubicNurbsError {
/// represent a much more diverse class of curves (like perfect circles and ellipses).
///
/// ### Non-uniformity
///
/// The 'NU' part of NURBS stands for "Non-Uniform". This has to do with a parameter called 'knots'.
/// The knots are a non-decreasing sequence of floating point numbers. The first and last three pairs of
/// knots control the behavior of the curve as it approaches its endpoints. The intermediate pairs
@ -572,16 +579,20 @@ pub enum CubicNurbsError {
/// and can create sharp corners.
///
/// ### Rationality
///
/// The 'R' part of NURBS stands for "Rational". This has to do with NURBS allowing each control point to
/// be assigned a weighting, which controls how much it affects the curve compared to the other points.
///
/// ### Interpolation
///
/// The curve will not pass through the control points except where a knot has multiplicity four.
///
/// ### Tangency
///
/// Tangents are automatically computed based on the position of control points.
///
/// ### Continuity
///
/// When there is no knot multiplicity, the curve is C2 continuous, meaning it has no holes or jumps and the
/// tangent vector changes smoothly along the entire curve length. Like the [`CubicBSpline`], the acceleration
/// continuity makes it useful for camera paths. Knot multiplicity of 2 in intermediate knots reduces the
@ -606,8 +617,8 @@ pub enum CubicNurbsError {
/// .unwrap();
/// let positions: Vec<_> = nurbs.iter_positions(100).collect();
/// ```
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicNurbs<P: VectorSpace> {
/// The control points of the NURBS
@ -822,21 +833,25 @@ impl<P: VectorSpace> RationalGenerator<P> for CubicNurbs<P> {
/// A spline interpolated linearly between the nearest 2 points.
///
/// ### Interpolation
///
/// The curve passes through every control point.
///
/// ### Tangency
///
/// The curve is not generally differentiable at control points.
///
/// ### Continuity
///
/// The curve is C0 continuous, meaning it has no holes or jumps.
///
/// ### Parametrization
///
/// Each curve segment connects two adjacent control points in sequence. When a cyclic curve is
/// formed with [`to_curve_cyclic`], the final segment connects the last control point with the first.
///
/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct LinearSpline<P: VectorSpace> {
/// The control points of the linear spline.
@ -945,6 +960,7 @@ pub trait CyclicCubicGenerator<P: VectorSpace> {
/// Segments can be chained together to form a longer [compound curve].
///
/// [compound curve]: CubicCurve
/// [`Curve`]: crate::curve::Curve
#[derive(Copy, Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Default))]
@ -1108,26 +1124,15 @@ impl CubicSegment<Vec2> {
}
}
#[cfg(feature = "curve")]
impl<P: VectorSpace> Curve<P> for CubicSegment<P> {
#[inline]
fn domain(&self) -> Interval {
Interval::UNIT
}
#[inline]
fn sample_unchecked(&self, t: f32) -> P {
self.position(t)
}
}
/// A collection of [`CubicSegment`]s chained into a single parametric curve. It is a [`Curve`]
/// with domain `[0, N]`, where `N` is its number of segments.
///
/// Use any struct that implements the [`CubicGenerator`] trait to create a new curve, such as
/// [`CubicBezier`].
#[cfg(feature = "alloc")]
///
/// [`Curve`]: crate::curve::Curve
#[derive(Clone, Debug, PartialEq)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicCurve<P: VectorSpace> {
@ -1248,21 +1253,6 @@ impl<P: VectorSpace> CubicCurve<P> {
}
}
#[cfg(all(feature = "curve", feature = "alloc"))]
impl<P: VectorSpace> Curve<P> for CubicCurve<P> {
#[inline]
fn domain(&self) -> Interval {
// The non-emptiness invariant guarantees the success of this.
Interval::new(0.0, self.segments.len() as f32)
.expect("CubicCurve is invalid because it has no segments")
}
#[inline]
fn sample_unchecked(&self, t: f32) -> P {
self.position(t)
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> Extend<CubicSegment<P>> for CubicCurve<P> {
fn extend<T: IntoIterator<Item = CubicSegment<P>>>(&mut self, iter: T) {
@ -1298,6 +1288,7 @@ pub trait RationalGenerator<P: VectorSpace> {
/// together.
///
/// [compound curves]: RationalCurve
/// [`Curve`]: crate::curve::Curve
#[derive(Copy, Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Default))]
@ -1309,7 +1300,6 @@ pub struct RationalSegment<P: VectorSpace> {
/// The width of the domain of this segment.
pub knot_span: f32,
}
impl<P: VectorSpace> RationalSegment<P> {
/// Instantaneous position of a point at parametric value `t` in `[0, 1]`.
#[inline]
@ -1424,26 +1414,15 @@ impl<P: VectorSpace> RationalSegment<P> {
}
}
#[cfg(feature = "curve")]
impl<P: VectorSpace> Curve<P> for RationalSegment<P> {
#[inline]
fn domain(&self) -> Interval {
Interval::UNIT
}
#[inline]
fn sample_unchecked(&self, t: f32) -> P {
self.position(t)
}
}
/// A collection of [`RationalSegment`]s chained into a single parametric curve. It is a [`Curve`]
/// with domain `[0, N]`, where `N` is the number of segments.
///
/// Use any struct that implements the [`RationalGenerator`] trait to create a new curve, such as
/// [`CubicNurbs`], or convert [`CubicCurve`] using `into/from`.
#[cfg(feature = "alloc")]
///
/// [`Curve`]: crate::curve::Curve
#[derive(Clone, Debug, PartialEq)]
#[cfg(feature = "alloc")]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct RationalCurve<P: VectorSpace> {
@ -1583,21 +1562,6 @@ impl<P: VectorSpace> RationalCurve<P> {
}
}
#[cfg(all(feature = "curve", feature = "alloc"))]
impl<P: VectorSpace> Curve<P> for RationalCurve<P> {
#[inline]
fn domain(&self) -> Interval {
// The non-emptiness invariant guarantees the success of this.
Interval::new(0.0, self.length())
.expect("RationalCurve is invalid because it has zero length")
}
#[inline]
fn sample_unchecked(&self, t: f32) -> P {
self.position(t)
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> Extend<RationalSegment<P>> for RationalCurve<P> {
fn extend<T: IntoIterator<Item = RationalSegment<P>>>(&mut self, iter: T) {

View file

@ -620,15 +620,25 @@ where
#[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());
let t = if t != d.start() && cyclic_t == 0.0 {
if t != d.start() && cyclic_t == 0.0 {
d.end()
} else {
d.start() + cyclic_t
};
self.curve.sample_unchecked(t)
}
}
}
@ -668,15 +678,25 @@ where
#[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());
let t = if t != d.start() && cyclic_t == 0.0 {
if t != d.start() && cyclic_t == 0.0 {
d.end()
} else {
d.start() + cyclic_t
};
self.curve.sample_unchecked(t)
}
}
}

View file

@ -0,0 +1,648 @@
//! Implementations of derivatives on curve adaptors. These allow
//! compositionality for derivatives.
use super::{SampleDerivative, SampleTwoDerivatives};
use crate::common_traits::{HasTangent, Sum, VectorSpace, WithDerivative, WithTwoDerivatives};
use crate::curve::{
adaptors::{
ChainCurve, ConstantCurve, ContinuationCurve, CurveReparamCurve, ForeverCurve, GraphCurve,
LinearReparamCurve, PingPongCurve, RepeatCurve, ReverseCurve, ZipCurve,
},
Curve,
};
// -- ConstantCurve
impl<T> SampleDerivative<T> for ConstantCurve<T>
where
T: HasTangent + Clone,
{
fn sample_with_derivative_unchecked(&self, _t: f32) -> WithDerivative<T> {
WithDerivative {
value: self.value.clone(),
derivative: VectorSpace::ZERO,
}
}
}
impl<T> SampleTwoDerivatives<T> for ConstantCurve<T>
where
T: HasTangent + Clone,
{
fn sample_with_two_derivatives_unchecked(&self, _t: f32) -> WithTwoDerivatives<T> {
WithTwoDerivatives {
value: self.value.clone(),
derivative: VectorSpace::ZERO,
second_derivative: VectorSpace::ZERO,
}
}
}
// -- ChainCurve
impl<T, C, D> SampleDerivative<T> for ChainCurve<T, C, D>
where
T: HasTangent,
C: SampleDerivative<T>,
D: SampleDerivative<T>,
{
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
if t > self.first.domain().end() {
self.second.sample_with_derivative_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_with_derivative_unchecked(t)
}
}
}
impl<T, C, D> SampleTwoDerivatives<T> for ChainCurve<T, C, D>
where
T: HasTangent,
C: SampleTwoDerivatives<T>,
D: SampleTwoDerivatives<T>,
{
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
if t > self.first.domain().end() {
self.second.sample_with_two_derivatives_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_with_two_derivatives_unchecked(t)
}
}
}
// -- ContinuationCurve
impl<T, C, D> SampleDerivative<T> for ContinuationCurve<T, C, D>
where
T: VectorSpace,
C: SampleDerivative<T>,
D: SampleDerivative<T>,
{
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
if t > self.first.domain().end() {
let mut output = self.second.sample_with_derivative_unchecked(
// `t - first.domain.end` computes the offset into the domain of the second.
t - self.first.domain().end() + self.second.domain().start(),
);
output.value = output.value + self.offset;
output
} else {
self.first.sample_with_derivative_unchecked(t)
}
}
}
impl<T, C, D> SampleTwoDerivatives<T> for ContinuationCurve<T, C, D>
where
T: VectorSpace,
C: SampleTwoDerivatives<T>,
D: SampleTwoDerivatives<T>,
{
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
if t > self.first.domain().end() {
let mut output = self.second.sample_with_two_derivatives_unchecked(
// `t - first.domain.end` computes the offset into the domain of the second.
t - self.first.domain().end() + self.second.domain().start(),
);
output.value = output.value + self.offset;
output
} else {
self.first.sample_with_two_derivatives_unchecked(t)
}
}
}
// -- RepeatCurve
impl<T, C> SampleDerivative<T> for RepeatCurve<T, C>
where
T: HasTangent,
C: SampleDerivative<T>,
{
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
let t = self.base_curve_sample_time(t);
self.curve.sample_with_derivative_unchecked(t)
}
}
impl<T, C> SampleTwoDerivatives<T> for RepeatCurve<T, C>
where
T: HasTangent,
C: SampleTwoDerivatives<T>,
{
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
let t = self.base_curve_sample_time(t);
self.curve.sample_with_two_derivatives_unchecked(t)
}
}
// -- ForeverCurve
impl<T, C> SampleDerivative<T> for ForeverCurve<T, C>
where
T: HasTangent,
C: SampleDerivative<T>,
{
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
let t = self.base_curve_sample_time(t);
self.curve.sample_with_derivative_unchecked(t)
}
}
impl<T, C> SampleTwoDerivatives<T> for ForeverCurve<T, C>
where
T: HasTangent,
C: SampleTwoDerivatives<T>,
{
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
let t = self.base_curve_sample_time(t);
self.curve.sample_with_two_derivatives_unchecked(t)
}
}
// -- PingPongCurve
impl<T, C> SampleDerivative<T> for PingPongCurve<T, C>
where
T: HasTangent,
C: SampleDerivative<T>,
{
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
if t > self.curve.domain().end() {
let t = self.curve.domain().end() * 2.0 - t;
// The derivative of the preceding expression is -1, so the chain
// rule implies the derivative should be negated.
let mut output = self.curve.sample_with_derivative_unchecked(t);
output.derivative = -output.derivative;
output
} else {
self.curve.sample_with_derivative_unchecked(t)
}
}
}
impl<T, C> SampleTwoDerivatives<T> for PingPongCurve<T, C>
where
T: HasTangent,
C: SampleTwoDerivatives<T>,
{
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
if t > self.curve.domain().end() {
let t = self.curve.domain().end() * 2.0 - t;
// See the implementation on `ReverseCurve` for an explanation of
// why this is correct.
let mut output = self.curve.sample_with_two_derivatives_unchecked(t);
output.derivative = -output.derivative;
output
} else {
self.curve.sample_with_two_derivatives_unchecked(t)
}
}
}
// -- ZipCurve
impl<S, T, C, D> SampleDerivative<(S, T)> for ZipCurve<S, T, C, D>
where
S: HasTangent,
T: HasTangent,
C: SampleDerivative<S>,
D: SampleDerivative<T>,
{
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<(S, T)> {
let first_output = self.first.sample_with_derivative_unchecked(t);
let second_output = self.second.sample_with_derivative_unchecked(t);
WithDerivative {
value: (first_output.value, second_output.value),
derivative: Sum(first_output.derivative, second_output.derivative),
}
}
}
impl<S, T, C, D> SampleTwoDerivatives<(S, T)> for ZipCurve<S, T, C, D>
where
S: HasTangent,
T: HasTangent,
C: SampleTwoDerivatives<S>,
D: SampleTwoDerivatives<T>,
{
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<(S, T)> {
let first_output = self.first.sample_with_two_derivatives_unchecked(t);
let second_output = self.second.sample_with_two_derivatives_unchecked(t);
WithTwoDerivatives {
value: (first_output.value, second_output.value),
derivative: Sum(first_output.derivative, second_output.derivative),
second_derivative: Sum(
first_output.second_derivative,
second_output.second_derivative,
),
}
}
}
// -- GraphCurve
impl<T, C> SampleDerivative<(f32, T)> for GraphCurve<T, C>
where
T: HasTangent,
C: SampleDerivative<T>,
{
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<(f32, T)> {
let output = self.base.sample_with_derivative_unchecked(t);
WithDerivative {
value: (t, output.value),
derivative: Sum(1.0, output.derivative),
}
}
}
impl<T, C> SampleTwoDerivatives<(f32, T)> for GraphCurve<T, C>
where
T: HasTangent,
C: SampleTwoDerivatives<T>,
{
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<(f32, T)> {
let output = self.base.sample_with_two_derivatives_unchecked(t);
WithTwoDerivatives {
value: (t, output.value),
derivative: Sum(1.0, output.derivative),
second_derivative: Sum(0.0, output.second_derivative),
}
}
}
// -- ReverseCurve
impl<T, C> SampleDerivative<T> for ReverseCurve<T, C>
where
T: HasTangent,
C: SampleDerivative<T>,
{
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
// This gets almost the correct value, but we haven't accounted for the
// reversal of orientation yet.
let mut output = self
.curve
.sample_with_derivative_unchecked(self.domain().end() - (t - self.domain().start()));
output.derivative = -output.derivative;
output
}
}
impl<T, C> SampleTwoDerivatives<T> for ReverseCurve<T, C>
where
T: HasTangent,
C: SampleTwoDerivatives<T>,
{
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
// This gets almost the correct value, but we haven't accounted for the
// reversal of orientation yet.
let mut output = self.curve.sample_with_two_derivatives_unchecked(
self.domain().end() - (t - self.domain().start()),
);
output.derivative = -output.derivative;
// (Note that the reparametrization that reverses the curve satisfies
// g'(t)^2 = 1 and g''(t) = 0, so the second derivative is already
// correct.)
output
}
}
// -- CurveReparamCurve
impl<T, C, D> SampleDerivative<T> for CurveReparamCurve<T, C, D>
where
T: HasTangent,
C: SampleDerivative<T>,
D: SampleDerivative<f32>,
{
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
// This curve is r(t) = f(g(t)), where f(t) is `self.base` and g(t)
// is `self.reparam_curve`.
// Start by computing g(t) and g'(t).
let reparam_output = self.reparam_curve.sample_with_derivative_unchecked(t);
// Compute:
// - value: f(g(t))
// - derivative: f'(g(t))
let mut output = self
.base
.sample_with_derivative_unchecked(reparam_output.value);
// Do the multiplication part of the chain rule.
output.derivative = output.derivative * reparam_output.derivative;
// value: f(g(t)), derivative: f'(g(t)) g'(t)
output
}
}
impl<T, C, D> SampleTwoDerivatives<T> for CurveReparamCurve<T, C, D>
where
T: HasTangent,
C: SampleTwoDerivatives<T>,
D: SampleTwoDerivatives<f32>,
{
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
// This curve is r(t) = f(g(t)), where f(t) is `self.base` and g(t)
// is `self.reparam_curve`.
// Start by computing g(t), g'(t), g''(t).
let reparam_output = self.reparam_curve.sample_with_two_derivatives_unchecked(t);
// Compute:
// - value: f(g(t))
// - derivative: f'(g(t))
// - second derivative: f''(g(t))
let mut output = self
.base
.sample_with_two_derivatives_unchecked(reparam_output.value);
// Set the second derivative according to the chain and product rules
// r''(t) = f''(g(t)) g'(t)^2 + f'(g(t)) g''(t)
output.second_derivative = (output.second_derivative
* (reparam_output.derivative * reparam_output.derivative))
+ (output.derivative * reparam_output.second_derivative);
// Set the first derivative according to the chain rule
// r'(t) = f'(g(t)) g'(t)
output.derivative = output.derivative * reparam_output.derivative;
output
}
}
// -- LinearReparamCurve
impl<T, C> SampleDerivative<T> for LinearReparamCurve<T, C>
where
T: HasTangent,
C: SampleDerivative<T>,
{
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
// This curve is r(t) = f(g(t)), where f(t) is `self.base` and g(t) is
// the linear map bijecting `self.new_domain` onto `self.base.domain()`.
// The invariants imply this unwrap always succeeds.
let g = self.new_domain.linear_map_to(self.base.domain()).unwrap();
// Compute g'(t) from the domain lengths.
let g_derivative = self.base.domain().length() / self.new_domain.length();
// Compute:
// - value: f(g(t))
// - derivative: f'(g(t))
let mut output = self.base.sample_with_derivative_unchecked(g(t));
// Adjust the derivative according to the chain rule.
output.derivative = output.derivative * g_derivative;
output
}
}
impl<T, C> SampleTwoDerivatives<T> for LinearReparamCurve<T, C>
where
T: HasTangent,
C: SampleTwoDerivatives<T>,
{
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
// This curve is r(t) = f(g(t)), where f(t) is `self.base` and g(t) is
// the linear map bijecting `self.new_domain` onto `self.base.domain()`.
// The invariants imply this unwrap always succeeds.
let g = self.new_domain.linear_map_to(self.base.domain()).unwrap();
// Compute g'(t) from the domain lengths.
let g_derivative = self.base.domain().length() / self.new_domain.length();
// Compute:
// - value: f(g(t))
// - derivative: f'(g(t))
// - second derivative: f''(g(t))
let mut output = self.base.sample_with_two_derivatives_unchecked(g(t));
// Set the second derivative according to the chain and product rules
// r''(t) = f''(g(t)) g'(t)^2 (g''(t) = 0)
output.second_derivative = output.second_derivative * (g_derivative * g_derivative);
// Set the first derivative according to the chain rule
// r'(t) = f'(g(t)) g'(t)
output.derivative = output.derivative * g_derivative;
output
}
}
#[cfg(test)]
mod tests {
use approx::assert_abs_diff_eq;
use super::*;
use crate::cubic_splines::{CubicBezier, CubicCardinalSpline, CubicCurve, CubicGenerator};
use crate::curve::{Curve, Interval};
use crate::{vec2, Vec2, Vec3};
fn test_curve() -> CubicCurve<Vec2> {
let control_pts = [[
vec2(0.0, 0.0),
vec2(1.0, 0.0),
vec2(0.0, 1.0),
vec2(1.0, 1.0),
]];
CubicBezier::new(control_pts).to_curve().unwrap()
}
fn other_test_curve() -> CubicCurve<Vec2> {
let control_pts = [
vec2(1.0, 1.0),
vec2(2.0, 1.0),
vec2(2.0, 0.0),
vec2(1.0, 0.0),
];
CubicCardinalSpline::new(0.5, control_pts)
.to_curve()
.unwrap()
}
fn reparam_curve() -> CubicCurve<f32> {
let control_pts = [[0.0, 0.25, 0.75, 1.0]];
CubicBezier::new(control_pts).to_curve().unwrap()
}
#[test]
fn constant_curve() {
let curve = ConstantCurve::new(Interval::UNIT, Vec3::new(0.2, 1.5, -2.6));
let jet = curve.sample_with_derivative(0.5).unwrap();
assert_abs_diff_eq!(jet.derivative, Vec3::ZERO);
}
#[test]
fn chain_curve() {
let curve1 = test_curve();
let curve2 = other_test_curve();
let curve = curve1.by_ref().chain(&curve2).unwrap();
let jet = curve.sample_with_derivative(0.65).unwrap();
let true_jet = curve1.sample_with_derivative(0.65).unwrap();
assert_abs_diff_eq!(jet.value, true_jet.value);
assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
let jet = curve.sample_with_derivative(1.1).unwrap();
let true_jet = curve2.sample_with_derivative(0.1).unwrap();
assert_abs_diff_eq!(jet.value, true_jet.value);
assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
}
#[test]
fn continuation_curve() {
let curve1 = test_curve();
let curve2 = other_test_curve();
let curve = curve1.by_ref().chain_continue(&curve2).unwrap();
let jet = curve.sample_with_derivative(0.99).unwrap();
let true_jet = curve1.sample_with_derivative(0.99).unwrap();
assert_abs_diff_eq!(jet.value, true_jet.value);
assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
let jet = curve.sample_with_derivative(1.3).unwrap();
let true_jet = curve2.sample_with_derivative(0.3).unwrap();
assert_abs_diff_eq!(jet.value, true_jet.value);
assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
}
#[test]
fn repeat_curve() {
let curve1 = test_curve();
let curve = curve1.by_ref().repeat(3).unwrap();
let jet = curve.sample_with_derivative(0.73).unwrap();
let true_jet = curve1.sample_with_derivative(0.73).unwrap();
assert_abs_diff_eq!(jet.value, true_jet.value);
assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
let jet = curve.sample_with_derivative(3.5).unwrap();
let true_jet = curve1.sample_with_derivative(0.5).unwrap();
assert_abs_diff_eq!(jet.value, true_jet.value);
assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
}
#[test]
fn forever_curve() {
let curve1 = test_curve();
let curve = curve1.by_ref().forever().unwrap();
let jet = curve.sample_with_derivative(0.12).unwrap();
let true_jet = curve1.sample_with_derivative(0.12).unwrap();
assert_abs_diff_eq!(jet.value, true_jet.value);
assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
let jet = curve.sample_with_derivative(500.5).unwrap();
let true_jet = curve1.sample_with_derivative(0.5).unwrap();
assert_abs_diff_eq!(jet.value, true_jet.value);
assert_abs_diff_eq!(jet.derivative, true_jet.derivative);
}
#[test]
fn ping_pong_curve() {
let curve1 = test_curve();
let curve = curve1.by_ref().ping_pong().unwrap();
let jet = curve.sample_with_derivative(0.99).unwrap();
let comparison_jet = curve1.sample_with_derivative(0.99).unwrap();
assert_abs_diff_eq!(jet.value, comparison_jet.value);
assert_abs_diff_eq!(jet.derivative, comparison_jet.derivative);
let jet = curve.sample_with_derivative(1.3).unwrap();
let comparison_jet = curve1.sample_with_derivative(0.7).unwrap();
assert_abs_diff_eq!(jet.value, comparison_jet.value);
assert_abs_diff_eq!(jet.derivative, -comparison_jet.derivative, epsilon = 1.0e-5);
}
#[test]
fn zip_curve() {
let curve1 = test_curve();
let curve2 = other_test_curve();
let curve = curve1.by_ref().zip(&curve2).unwrap();
let jet = curve.sample_with_derivative(0.7).unwrap();
let comparison_jet1 = curve1.sample_with_derivative(0.7).unwrap();
let comparison_jet2 = curve2.sample_with_derivative(0.7).unwrap();
assert_abs_diff_eq!(jet.value.0, comparison_jet1.value);
assert_abs_diff_eq!(jet.value.1, comparison_jet2.value);
let Sum(derivative1, derivative2) = jet.derivative;
assert_abs_diff_eq!(derivative1, comparison_jet1.derivative);
assert_abs_diff_eq!(derivative2, comparison_jet2.derivative);
}
#[test]
fn graph_curve() {
let curve1 = test_curve();
let curve = curve1.by_ref().graph();
let jet = curve.sample_with_derivative(0.25).unwrap();
let comparison_jet = curve1.sample_with_derivative(0.25).unwrap();
assert_abs_diff_eq!(jet.value.0, 0.25);
assert_abs_diff_eq!(jet.value.1, comparison_jet.value);
let Sum(one, derivative) = jet.derivative;
assert_abs_diff_eq!(one, 1.0);
assert_abs_diff_eq!(derivative, comparison_jet.derivative);
}
#[test]
fn reverse_curve() {
let curve1 = test_curve();
let curve = curve1.by_ref().reverse().unwrap();
let jet = curve.sample_with_derivative(0.23).unwrap();
let comparison_jet = curve1.sample_with_derivative(0.77).unwrap();
assert_abs_diff_eq!(jet.value, comparison_jet.value);
assert_abs_diff_eq!(jet.derivative, -comparison_jet.derivative);
}
#[test]
fn curve_reparam_curve() {
let reparam_curve = reparam_curve();
let reparam_jet = reparam_curve.sample_with_derivative(0.6).unwrap();
let curve1 = test_curve();
let curve = curve1.by_ref().reparametrize_by_curve(&reparam_curve);
let jet = curve.sample_with_derivative(0.6).unwrap();
let base_jet = curve1
.sample_with_derivative(reparam_curve.sample(0.6).unwrap())
.unwrap();
assert_abs_diff_eq!(jet.value, base_jet.value);
assert_abs_diff_eq!(jet.derivative, base_jet.derivative * reparam_jet.derivative);
}
#[test]
fn linear_reparam_curve() {
let curve1 = test_curve();
let curve = curve1
.by_ref()
.reparametrize_linear(Interval::new(0.0, 0.5).unwrap())
.unwrap();
let jet = curve.sample_with_derivative(0.23).unwrap();
let comparison_jet = curve1.sample_with_derivative(0.46).unwrap();
assert_abs_diff_eq!(jet.value, comparison_jet.value);
assert_abs_diff_eq!(jet.derivative, comparison_jet.derivative * 2.0);
}
}

View file

@ -0,0 +1,226 @@
//! This module holds traits related to extracting derivatives from curves. In
//! applications, the derivatives of interest are chiefly the first and second;
//! in this module, these are provided by the traits [`CurveWithDerivative`]
//! and [`CurveWithTwoDerivatives`].
//!
//! These take ownership of the curve they are used on by default, so that
//! the resulting output may be used in more durable contexts. For example,
//! `CurveWithDerivative<T>` is not dyn-compatible, but `Curve<WithDerivative<T>>`
//! is, so if such a curve needs to be stored in a dynamic context, calling
//! [`with_derivative`] and then placing the result in a
//! `Box<Curve<WithDerivative<T>>>` is sensible.
//!
//! On the other hand, in more transient contexts, consuming a value merely to
//! sample derivatives is inconvenient, and in these cases, it is recommended
//! to use [`by_ref`] when possible to create a referential curve first, retaining
//! liveness of the original.
//!
//! This module also holds the [`SampleDerivative`] and [`SampleTwoDerivatives`]
//! traits, which can be used to easily implement `CurveWithDerivative` and its
//! counterpart.
//!
//! [`with_derivative`]: CurveWithDerivative::with_derivative
//! [`by_ref`]: Curve::by_ref
pub mod adaptor_impls;
use crate::{
common_traits::{HasTangent, WithDerivative, WithTwoDerivatives},
curve::{Curve, Interval},
};
use core::ops::Deref;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::{FromReflect, Reflect};
/// Trait for curves that have a well-defined notion of derivative, allowing for
/// derivatives to be extracted along with values.
///
/// This is implemented by implementing [`SampleDerivative`].
pub trait CurveWithDerivative<T>: SampleDerivative<T>
where
T: HasTangent,
{
/// This curve, but with its first derivative included in sampling.
fn with_derivative(self) -> impl Curve<WithDerivative<T>>;
}
/// Trait for curves that have a well-defined notion of second derivative,
/// allowing for two derivatives to be extracted along with values.
///
/// This is implemented by implementing [`SampleTwoDerivatives`].
pub trait CurveWithTwoDerivatives<T>: SampleTwoDerivatives<T>
where
T: HasTangent,
{
/// This curve, but with its first two derivatives included in sampling.
fn with_two_derivatives(self) -> impl Curve<WithTwoDerivatives<T>>;
}
/// A trait for curves that can sample derivatives in addition to values.
///
/// Types that implement this trait automatically implement [`CurveWithDerivative`];
/// the curve produced by [`with_derivative`] uses the sampling defined in the trait
/// implementation.
///
/// [`with_derivative`]: CurveWithDerivative::with_derivative
pub trait SampleDerivative<T>: Curve<T>
where
T: HasTangent,
{
/// Sample this curve at the parameter value `t`, extracting the associated value
/// in addition to its derivative. This is the unchecked version of sampling, which
/// should only be used if the sample time `t` is already known to lie within the
/// curve's domain.
///
/// See [`Curve::sample_unchecked`] for more information.
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T>;
/// Sample this curve's value and derivative at the parameter value `t`, returning
/// `None` if the point is outside of the curve's domain.
fn sample_with_derivative(&self, t: f32) -> Option<WithDerivative<T>> {
match self.domain().contains(t) {
true => Some(self.sample_with_derivative_unchecked(t)),
false => None,
}
}
/// Sample this curve's value and derivative at the parameter value `t`, clamping `t`
/// to lie inside the domain of the curve.
fn sample_with_derivative_clamped(&self, t: f32) -> WithDerivative<T> {
let t = self.domain().clamp(t);
self.sample_with_derivative_unchecked(t)
}
}
impl<T, C, D> SampleDerivative<T> for D
where
T: HasTangent,
C: SampleDerivative<T> + ?Sized,
D: Deref<Target = C>,
{
fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<T> {
<C as SampleDerivative<T>>::sample_with_derivative_unchecked(self, t)
}
}
/// A trait for curves that can sample two derivatives in addition to values.
///
/// Types that implement this trait automatically implement [`CurveWithTwoDerivatives`];
/// the curve produced by [`with_two_derivatives`] uses the sampling defined in the trait
/// implementation.
///
/// [`with_two_derivatives`]: CurveWithTwoDerivatives::with_two_derivatives
pub trait SampleTwoDerivatives<T>: Curve<T>
where
T: HasTangent,
{
/// Sample this curve at the parameter value `t`, extracting the associated value
/// in addition to two derivatives. This is the unchecked version of sampling, which
/// should only be used if the sample time `t` is already known to lie within the
/// curve's domain.
///
/// See [`Curve::sample_unchecked`] for more information.
fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<T>;
/// Sample this curve's value and two derivatives at the parameter value `t`, returning
/// `None` if the point is outside of the curve's domain.
fn sample_with_two_derivatives(&self, t: f32) -> Option<WithTwoDerivatives<T>> {
match self.domain().contains(t) {
true => Some(self.sample_with_two_derivatives_unchecked(t)),
false => None,
}
}
/// Sample this curve's value and two derivatives at the parameter value `t`, clamping `t`
/// to lie inside the domain of the curve.
fn sample_with_two_derivatives_clamped(&self, t: f32) -> WithTwoDerivatives<T> {
let t = self.domain().clamp(t);
self.sample_with_two_derivatives_unchecked(t)
}
}
/// A wrapper that uses a [`SampleDerivative<T>`] curve to produce a `Curve<WithDerivative<T>>`.
#[derive(Copy, Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect, FromReflect),
reflect(from_reflect = false)
)]
pub struct SampleDerivativeWrapper<C>(C);
impl<T, C> Curve<WithDerivative<T>> for SampleDerivativeWrapper<C>
where
T: HasTangent,
C: SampleDerivative<T>,
{
fn domain(&self) -> Interval {
self.0.domain()
}
fn sample_unchecked(&self, t: f32) -> WithDerivative<T> {
self.0.sample_with_derivative_unchecked(t)
}
fn sample(&self, t: f32) -> Option<WithDerivative<T>> {
self.0.sample_with_derivative(t)
}
fn sample_clamped(&self, t: f32) -> WithDerivative<T> {
self.0.sample_with_derivative_clamped(t)
}
}
/// A wrapper that uses a [`SampleTwoDerivatives<T>`] curve to produce a
/// `Curve<WithTwoDerivatives<T>>`.
#[derive(Copy, Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect, FromReflect),
reflect(from_reflect = false)
)]
pub struct SampleTwoDerivativesWrapper<C>(C);
impl<T, C> Curve<WithTwoDerivatives<T>> for SampleTwoDerivativesWrapper<C>
where
T: HasTangent,
C: SampleTwoDerivatives<T>,
{
fn domain(&self) -> Interval {
self.0.domain()
}
fn sample_unchecked(&self, t: f32) -> WithTwoDerivatives<T> {
self.0.sample_with_two_derivatives_unchecked(t)
}
fn sample(&self, t: f32) -> Option<WithTwoDerivatives<T>> {
self.0.sample_with_two_derivatives(t)
}
fn sample_clamped(&self, t: f32) -> WithTwoDerivatives<T> {
self.0.sample_with_two_derivatives_clamped(t)
}
}
impl<T, C> CurveWithDerivative<T> for C
where
T: HasTangent,
C: SampleDerivative<T>,
{
fn with_derivative(self) -> impl Curve<WithDerivative<T>> {
SampleDerivativeWrapper(self)
}
}
impl<T, C> CurveWithTwoDerivatives<T> for C
where
T: HasTangent,
C: SampleTwoDerivatives<T> + CurveWithDerivative<T>,
{
fn with_two_derivatives(self) -> impl Curve<WithTwoDerivatives<T>> {
SampleTwoDerivativesWrapper(self)
}
}

View file

@ -287,6 +287,7 @@
pub mod adaptors;
pub mod cores;
pub mod derivatives;
pub mod easing;
pub mod interval;
pub mod iterable;

View file

@ -16,17 +16,18 @@ TOI = "TOI" # Time of impact
locale = "en-us"
# Ignored typos regexes
extend-ignore-identifiers-re = [
"Ba", # Bitangent for Anisotropy
"ba", # Part of an accessor in WGSL - color.ba
"ser", # ron::ser - Serializer
"SME", # Subject Matter Expert
"Sur", # macOS Big Sur - South
"NDK", # NDK - Native Development Kit
"PNG", # PNG - Portable Network Graphics file format
"Masia", # The surname of one of the authors of SMAA
"metalness", # Rendering term (metallicity)
"inventario", # Inventory in Portuguese
"[Rr]eparametrize", # Mathematical term in curve context (reparameterize)
"Ba", # Bitangent for Anisotropy
"ba", # Part of an accessor in WGSL - color.ba
"ser", # ron::ser - Serializer
"SME", # Subject Matter Expert
"Sur", # macOS Big Sur - South
"NDK", # NDK - Native Development Kit
"PNG", # PNG - Portable Network Graphics file format
"Masia", # The surname of one of the authors of SMAA
"metalness", # Rendering term (metallicity)
"inventario", # Inventory in Portuguese
"[Rr]eparametrize", # Mathematical term in curve context (reparameterize)
"[Rr]eparametrization",
# Used in bevy_mikktspace
"iFO",
"vOt",