Move Point out of cubic splines module and expand it (#12747)

# Objective

Previously, the `Point` trait, which abstracts all of the operations of
a real vector space, was sitting in the submodule of `bevy_math` for
cubic splines. However, the trait has broader applications than merely
cubic splines, and we should use it when possible to avoid code
duplication when performing vector operations.

## Solution

`Point` has been moved into a new submodule in `bevy_math` named
`common_traits`. Furthermore, it has been renamed to `VectorSpace`,
which is more descriptive, and an additional trait `NormedVectorSpace`
has been introduced to expand the API to cover situations involving
geometry in addition to algebra. Additionally, `VectorSpace` itself now
requires a `ZERO` constant and `Neg`. It also supports a `lerp` function
as an automatic trait method.

Here is what that looks like:
```rust
/// A type that supports the mathematical operations of a real vector space, irrespective of dimension.
/// In particular, this means that the implementing type supports:
/// - Scalar multiplication and division on the right by elements of `f32`
/// - Negation
/// - Addition and subtraction
/// - Zero
///
/// Within the limitations of floating point arithmetic, all the following are required to hold:
/// - (Associativity of addition) For all `u, v, w: Self`, `(u + v) + w == u + (v + w)`.
/// - (Commutativity of addition) For all `u, v: Self`, `u + v == v + u`.
/// - (Additive identity) For all `v: Self`, `v + Self::ZERO == v`.
/// - (Additive inverse) For all `v: Self`, `v - v == v + (-v) == Self::ZERO`.
/// - (Compatibility of multiplication) For all `a, b: f32`, `v: Self`, `v * (a * b) == (v * a) * b`.
/// - (Multiplicative identity) For all `v: Self`, `v * 1.0 == v`.
/// - (Distributivity for vector addition) For all `a: f32`, `u, v: Self`, `(u + v) * a == u * a + v * a`.
/// - (Distributivity for scalar addition) For all `a, b: f32`, `v: Self`, `v * (a + b) == v * a + v * b`.
///
/// Note that, because implementing types use floating point arithmetic, they are not required to actually
/// implement `PartialEq` or `Eq`.
pub trait VectorSpace:
    Mul<f32, Output = Self>
    + Div<f32, Output = Self>
    + Add<Self, Output = Self>
    + Sub<Self, Output = Self>
    + Neg
    + Default
    + Debug
    + Clone
    + Copy
{
    /// The zero vector, which is the identity of addition for the vector space type.
    const ZERO: Self;

    /// Perform vector space linear interpolation between this element and another, based
    /// on the parameter `t`. When `t` is `0`, `self` is recovered. When `t` is `1`, `rhs`
    /// is recovered.
    ///
    /// Note that the value of `t` is not clamped by this function, so interpolating outside
    /// of the interval `[0,1]` is allowed.
    #[inline]
    fn lerp(&self, rhs: Self, t: f32) -> Self {
        *self * (1. - t) + rhs * t
    }
}
```
```rust
/// 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:
/// - (Nonnegativity) For all `v: Self`, `v.norm() >= 0.0`.
/// - (Positive definiteness) For all `v: Self`, `v.norm() == 0.0` implies `v == Self::ZERO`.
/// - (Absolute homogeneity) For all `c: f32`, `v: Self`, `(v * c).norm() == v.norm() * c.abs()`.
/// - (Triangle inequality) For all `v, w: Self`, `(v + w).norm() <= v.norm() + w.norm()`.
///
/// Note that, because implementing types use floating point arithmetic, they are not required to actually
/// implement `PartialEq` or `Eq`.
pub trait NormedVectorSpace: VectorSpace {
    /// The size of this element. The return value should always be nonnegative.
    fn norm(self) -> f32;

    /// The squared norm of this element. Computing this is often faster than computing
    /// [`NormedVectorSpace::norm`].
    #[inline]
    fn norm_squared(self) -> f32 {
        self.norm() * self.norm()
    }

    /// The distance between this element and another, as determined by the norm.
    #[inline]
    fn distance(self, rhs: Self) -> f32 {
        (rhs - self).norm()
    }

    /// The squared distance between this element and another, as determined by the norm. Note that
    /// this is often faster to compute in practice than [`NormedVectorSpace::distance`].
    #[inline]
    fn distance_squared(self, rhs: Self) -> f32 {
        (rhs - self).norm_squared()
    }
}
```

Furthermore, this PR also demonstrates the use of the
`NormedVectorSpace` combined API to implement `ShapeSample` for
`Triangle2d` and `Triangle3d` simultaneously. Such deduplication is one
of the drivers for developing these APIs.

---

## Changelog

- `Point` from `cubic_splines` becomes `VectorSpace`, exported as
`bevy::math::VectorSpace`.
- `VectorSpace` requires `Neg` and `VectorSpace::ZERO` in addition to
its existing prerequisites.
- Introduced public traits `bevy::math::NormedVectorSpace` for generic
geometry tasks involving vectors.
- Implemented `ShapeSample` for `Triangle2d` and `Triangle3d`.

## Migration Guide

Since `Point` no longer exists, any projects using it must switch to
`bevy::math::VectorSpace`. Additionally, third-party implementations of
this trait now require the `Neg` trait; the constant `VectorSpace::ZERO`
must be provided as well.

---

## Discussion

### Design considerations

Originally, the `NormedVectorSpace::norm` method was part of a separate
trait `Normed`. However, I think that was probably too broad and, more
importantly, the semantics of having it in `NormedVectorSpace` are much
clearer.

As it currently stands, the API exposed here is pretty minimal, and
there is definitely a lot more that we could do, but there are more
questions to answer along the way. As a silly example, we could
implement `NormedVectorSpace::length` as an alias for
`NormedVectorSpace::norm`, but this overlaps with methods in all of the
glam types, so we would want to make sure that the implementations are
effectively identical (for what it's worth, I think they are already).

### Future directions

One example of something that could belong in the `NormedVectorSpace`
API is normalization. Actually, such a thing previously existed on this
branch before I decided to shelve it because of concerns with namespace
collision. It looked like this:
```rust
/// This element, but normalized to norm 1 if possible. Returns an error when the reciprocal of
/// the element's norm is not finite.
#[inline]
#[must_use]
fn normalize(&self) -> Result<Self, NonNormalizableError> {
    let reciprocal = 1.0 / self.norm();
    if reciprocal.is_finite() {
        Ok(*self * reciprocal)
    } else {
        Err(NonNormalizableError { reciprocal })
    }
}

/// An error indicating that an element of a [`NormedVectorSpace`] was non-normalizable due to having 
/// non-finite norm-reciprocal.
#[derive(Debug, Error)]
#[error("Element with norm reciprocal {reciprocal} cannot be normalized")]
pub struct NonNormalizableError {
    reciprocal: f32
}
```

With this kind of thing in hand, it might be worth considering
eventually making the passage from vectors to directions fully generic
by employing a wrapper type. (Of course, for our concrete types, we
would leave the existing names in place as aliases.) That is, something
like:
```rust
pub struct NormOne<T>
where T: NormedVectorSpace { //... }
```

Utterly separately, the reason that I implemented `ShapeSample` for
`Triangle2d`/`Triangle3d` was to prototype uniform sampling of abstract
meshes, so that's also a future direction.

---------

Co-authored-by: Zachary Harrold <zac@harrold.com.au>
This commit is contained in:
Matty 2024-03-28 09:40:26 -04:00 committed by GitHub
parent 760c645de1
commit f924b4d9ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 327 additions and 81 deletions

View file

@ -1,6 +1,6 @@
use crate::{
impl_componentwise_point, Alpha, ClampColor, Hsla, Hsva, Hwba, LinearRgba, Luminance, Mix,
Oklaba, Srgba, StandardColor, Xyza,
impl_componentwise_vector_space, Alpha, ClampColor, Hsla, Hsva, Hwba, LinearRgba, Luminance,
Mix, Oklaba, Srgba, StandardColor, Xyza,
};
use bevy_reflect::prelude::*;
@ -29,7 +29,7 @@ pub struct Laba {
impl StandardColor for Laba {}
impl_componentwise_point!(Laba, [lightness, a, b, alpha]);
impl_componentwise_vector_space!(Laba, [lightness, a, b, alpha]);
impl Laba {
/// Construct a new [`Laba`] color from components.

View file

@ -164,7 +164,7 @@ where
{
}
macro_rules! impl_componentwise_point {
macro_rules! impl_componentwise_vector_space {
($ty: ident, [$($element: ident),+]) => {
impl std::ops::Add<Self> for $ty {
type Output = Self;
@ -182,6 +182,16 @@ macro_rules! impl_componentwise_point {
}
}
impl std::ops::Neg for $ty {
type Output = Self;
fn neg(self) -> Self::Output {
Self::Output {
$($element: -self.$element,)+
}
}
}
impl std::ops::Sub<Self> for $ty {
type Output = Self;
@ -240,8 +250,12 @@ macro_rules! impl_componentwise_point {
}
}
impl bevy_math::cubic_splines::Point for $ty {}
impl bevy_math::VectorSpace for $ty {
const ZERO: Self = Self {
$($element: 0.0,)+
};
}
};
}
pub(crate) use impl_componentwise_point;
pub(crate) use impl_componentwise_vector_space;

View file

@ -1,6 +1,6 @@
use crate::{
color_difference::EuclideanDistance, impl_componentwise_point, Alpha, ClampColor, Luminance,
Mix, StandardColor,
color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ClampColor,
Luminance, Mix, StandardColor,
};
use bevy_math::Vec4;
use bevy_reflect::prelude::*;
@ -32,7 +32,7 @@ pub struct LinearRgba {
impl StandardColor for LinearRgba {}
impl_componentwise_point!(LinearRgba, [red, green, blue, alpha]);
impl_componentwise_vector_space!(LinearRgba, [red, green, blue, alpha]);
impl LinearRgba {
/// A fully black color with full alpha.

View file

@ -1,6 +1,6 @@
use crate::{
color_difference::EuclideanDistance, impl_componentwise_point, Alpha, ClampColor, Hsla, Hsva,
Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza,
color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ClampColor, Hsla,
Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza,
};
use bevy_reflect::prelude::*;
@ -29,7 +29,7 @@ pub struct Oklaba {
impl StandardColor for Oklaba {}
impl_componentwise_point!(Oklaba, [lightness, a, b, alpha]);
impl_componentwise_vector_space!(Oklaba, [lightness, a, b, alpha]);
impl Oklaba {
/// Construct a new [`Oklaba`] color from components.

View file

@ -1,6 +1,7 @@
use crate::color_difference::EuclideanDistance;
use crate::{
impl_componentwise_point, Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor, Xyza,
impl_componentwise_vector_space, Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor,
Xyza,
};
use bevy_math::Vec4;
use bevy_reflect::prelude::*;
@ -31,7 +32,7 @@ pub struct Srgba {
impl StandardColor for Srgba {}
impl_componentwise_point!(Srgba, [red, green, blue, alpha]);
impl_componentwise_vector_space!(Srgba, [red, green, blue, alpha]);
impl Srgba {
// The standard VGA colors, with alpha set to 1.0.

View file

@ -1,5 +1,5 @@
use crate::{
impl_componentwise_point, Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor,
impl_componentwise_vector_space, Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor,
};
use bevy_reflect::prelude::*;
@ -28,7 +28,7 @@ pub struct Xyza {
impl StandardColor for Xyza {}
impl_componentwise_point!(Xyza, [x, y, z, alpha]);
impl_componentwise_vector_space!(Xyza, [x, y, z, alpha]);
impl Xyza {
/// Construct a new [`Xyza`] color from components.

View file

@ -0,0 +1,180 @@
use glam::{Quat, Vec2, Vec3, Vec3A, Vec4};
use std::fmt::Debug;
use std::ops::{Add, Div, Mul, Neg, Sub};
/// A type that supports the mathematical operations of a real vector space, irrespective of dimension.
/// In particular, this means that the implementing type supports:
/// - Scalar multiplication and division on the right by elements of `f32`
/// - Negation
/// - Addition and subtraction
/// - Zero
///
/// Within the limitations of floating point arithmetic, all the following are required to hold:
/// - (Associativity of addition) For all `u, v, w: Self`, `(u + v) + w == u + (v + w)`.
/// - (Commutativity of addition) For all `u, v: Self`, `u + v == v + u`.
/// - (Additive identity) For all `v: Self`, `v + Self::ZERO == v`.
/// - (Additive inverse) For all `v: Self`, `v - v == v + (-v) == Self::ZERO`.
/// - (Compatibility of multiplication) For all `a, b: f32`, `v: Self`, `v * (a * b) == (v * a) * b`.
/// - (Multiplicative identity) For all `v: Self`, `v * 1.0 == v`.
/// - (Distributivity for vector addition) For all `a: f32`, `u, v: Self`, `(u + v) * a == u * a + v * a`.
/// - (Distributivity for scalar addition) For all `a, b: f32`, `v: Self`, `v * (a + b) == v * a + v * b`.
///
/// Note that, because implementing types use floating point arithmetic, they are not required to actually
/// implement `PartialEq` or `Eq`.
pub trait VectorSpace:
Mul<f32, Output = Self>
+ Div<f32, Output = Self>
+ Add<Self, Output = Self>
+ Sub<Self, Output = Self>
+ Neg
+ Default
+ Debug
+ Clone
+ Copy
{
/// The zero vector, which is the identity of addition for the vector space type.
const ZERO: Self;
/// Perform vector space linear interpolation between this element and another, based
/// on the parameter `t`. When `t` is `0`, `self` is recovered. When `t` is `1`, `rhs`
/// is recovered.
///
/// Note that the value of `t` is not clamped by this function, so interpolating outside
/// of the interval `[0,1]` is allowed.
#[inline]
fn lerp(&self, rhs: Self, t: f32) -> Self {
*self * (1. - t) + rhs * t
}
}
// This is cursed and we should probably remove Quat from these.
impl VectorSpace for Quat {
const ZERO: Self = Quat::from_xyzw(0., 0., 0., 0.);
}
impl VectorSpace for Vec4 {
const ZERO: Self = Vec4::ZERO;
}
impl VectorSpace for Vec3 {
const ZERO: Self = Vec3::ZERO;
}
impl VectorSpace for Vec3A {
const ZERO: Self = Vec3A::ZERO;
}
impl VectorSpace for Vec2 {
const ZERO: Self = Vec2::ZERO;
}
impl VectorSpace for f32 {
const ZERO: Self = 0.0;
}
/// 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:
/// - (Nonnegativity) For all `v: Self`, `v.norm() >= 0.0`.
/// - (Positive definiteness) For all `v: Self`, `v.norm() == 0.0` implies `v == Self::ZERO`.
/// - (Absolute homogeneity) For all `c: f32`, `v: Self`, `(v * c).norm() == v.norm() * c.abs()`.
/// - (Triangle inequality) For all `v, w: Self`, `(v + w).norm() <= v.norm() + w.norm()`.
///
/// Note that, because implementing types use floating point arithmetic, they are not required to actually
/// implement `PartialEq` or `Eq`.
pub trait NormedVectorSpace: VectorSpace {
/// The size of this element. The return value should always be nonnegative.
fn norm(self) -> f32;
/// The squared norm of this element. Computing this is often faster than computing
/// [`NormedVectorSpace::norm`].
#[inline]
fn norm_squared(self) -> f32 {
self.norm() * self.norm()
}
/// The distance between this element and another, as determined by the norm.
#[inline]
fn distance(self, rhs: Self) -> f32 {
(rhs - self).norm()
}
/// The squared distance between this element and another, as determined by the norm. Note that
/// this is often faster to compute in practice than [`NormedVectorSpace::distance`].
#[inline]
fn distance_squared(self, rhs: Self) -> f32 {
(rhs - self).norm_squared()
}
}
impl NormedVectorSpace for Quat {
#[inline]
fn norm(self) -> f32 {
self.length()
}
#[inline]
fn norm_squared(self) -> f32 {
self.length_squared()
}
}
impl NormedVectorSpace for Vec4 {
#[inline]
fn norm(self) -> f32 {
self.length()
}
#[inline]
fn norm_squared(self) -> f32 {
self.length_squared()
}
}
impl NormedVectorSpace for Vec3 {
#[inline]
fn norm(self) -> f32 {
self.length()
}
#[inline]
fn norm_squared(self) -> f32 {
self.length_squared()
}
}
impl NormedVectorSpace for Vec3A {
#[inline]
fn norm(self) -> f32 {
self.length()
}
#[inline]
fn norm_squared(self) -> f32 {
self.length_squared()
}
}
impl NormedVectorSpace for Vec2 {
#[inline]
fn norm(self) -> f32 {
self.length()
}
#[inline]
fn norm_squared(self) -> f32 {
self.length_squared()
}
}
impl NormedVectorSpace for f32 {
#[inline]
fn norm(self) -> f32 {
self.abs()
}
#[inline]
fn norm_squared(self) -> f32 {
self * self
}
}

View file

@ -1,35 +1,11 @@
//! Provides types for building cubic splines for rendering curves and use with animation easing.
use std::{
fmt::Debug,
iter::once,
ops::{Add, Div, Mul, Sub},
};
use std::{fmt::Debug, iter::once};
use crate::{Vec2, VectorSpace};
use glam::{Quat, Vec2, Vec3, Vec3A, Vec4};
use thiserror::Error;
/// A point in space of any dimension that supports the math ops needed for cubic spline
/// interpolation.
pub trait Point:
Mul<f32, Output = Self>
+ Div<f32, Output = Self>
+ Add<Self, Output = Self>
+ Sub<Self, Output = Self>
+ Default
+ Debug
+ Clone
+ Copy
{
}
impl Point for Quat {}
impl Point for Vec4 {}
impl Point for Vec3 {}
impl Point for Vec3A {}
impl Point for Vec2 {}
impl Point for f32 {}
/// A spline composed of a single cubic Bezier curve.
///
/// Useful for user-drawn curves with local control, or animation easing. See
@ -64,11 +40,11 @@ impl Point for f32 {}
/// let bezier = CubicBezier::new(points).to_curve();
/// let positions: Vec<_> = bezier.iter_positions(100).collect();
/// ```
pub struct CubicBezier<P: Point> {
pub struct CubicBezier<P: VectorSpace> {
control_points: Vec<[P; 4]>,
}
impl<P: Point> CubicBezier<P> {
impl<P: VectorSpace> CubicBezier<P> {
/// Create a new cubic Bezier curve from sets of control points.
pub fn new(control_points: impl Into<Vec<[P; 4]>>) -> Self {
Self {
@ -76,7 +52,7 @@ impl<P: Point> CubicBezier<P> {
}
}
}
impl<P: Point> CubicGenerator<P> for CubicBezier<P> {
impl<P: VectorSpace> CubicGenerator<P> for CubicBezier<P> {
#[inline]
fn to_curve(&self) -> CubicCurve<P> {
// A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.
@ -135,10 +111,10 @@ impl<P: Point> CubicGenerator<P> for CubicBezier<P> {
/// let hermite = CubicHermite::new(points, tangents).to_curve();
/// let positions: Vec<_> = hermite.iter_positions(100).collect();
/// ```
pub struct CubicHermite<P: Point> {
pub struct CubicHermite<P: VectorSpace> {
control_points: Vec<(P, P)>,
}
impl<P: Point> CubicHermite<P> {
impl<P: VectorSpace> CubicHermite<P> {
/// Create a new Hermite curve from sets of control points.
pub fn new(
control_points: impl IntoIterator<Item = P>,
@ -149,7 +125,7 @@ impl<P: Point> CubicHermite<P> {
}
}
}
impl<P: Point> CubicGenerator<P> for CubicHermite<P> {
impl<P: VectorSpace> CubicGenerator<P> for CubicHermite<P> {
#[inline]
fn to_curve(&self) -> CubicCurve<P> {
let char_matrix = [
@ -201,12 +177,12 @@ impl<P: Point> CubicGenerator<P> for CubicHermite<P> {
/// let cardinal = CubicCardinalSpline::new(0.3, points).to_curve();
/// let positions: Vec<_> = cardinal.iter_positions(100).collect();
/// ```
pub struct CubicCardinalSpline<P: Point> {
pub struct CubicCardinalSpline<P: VectorSpace> {
tension: f32,
control_points: Vec<P>,
}
impl<P: Point> CubicCardinalSpline<P> {
impl<P: VectorSpace> CubicCardinalSpline<P> {
/// Build a new Cardinal spline.
pub fn new(tension: f32, control_points: impl Into<Vec<P>>) -> Self {
Self {
@ -223,7 +199,7 @@ impl<P: Point> CubicCardinalSpline<P> {
}
}
}
impl<P: Point> CubicGenerator<P> for CubicCardinalSpline<P> {
impl<P: VectorSpace> CubicGenerator<P> for CubicCardinalSpline<P> {
#[inline]
fn to_curve(&self) -> CubicCurve<P> {
let s = self.tension;
@ -288,10 +264,10 @@ impl<P: Point> CubicGenerator<P> for CubicCardinalSpline<P> {
/// let b_spline = CubicBSpline::new(points).to_curve();
/// let positions: Vec<_> = b_spline.iter_positions(100).collect();
/// ```
pub struct CubicBSpline<P: Point> {
pub struct CubicBSpline<P: VectorSpace> {
control_points: Vec<P>,
}
impl<P: Point> CubicBSpline<P> {
impl<P: VectorSpace> CubicBSpline<P> {
/// Build a new B-Spline.
pub fn new(control_points: impl Into<Vec<P>>) -> Self {
Self {
@ -299,7 +275,7 @@ impl<P: Point> CubicBSpline<P> {
}
}
}
impl<P: Point> CubicGenerator<P> for CubicBSpline<P> {
impl<P: VectorSpace> CubicGenerator<P> for CubicBSpline<P> {
#[inline]
fn to_curve(&self) -> CubicCurve<P> {
// A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.
@ -405,12 +381,12 @@ pub enum CubicNurbsError {
/// .to_curve();
/// let positions: Vec<_> = nurbs.iter_positions(100).collect();
/// ```
pub struct CubicNurbs<P: Point> {
pub struct CubicNurbs<P: VectorSpace> {
control_points: Vec<P>,
weights: Vec<f32>,
knots: Vec<f32>,
}
impl<P: Point> CubicNurbs<P> {
impl<P: VectorSpace> CubicNurbs<P> {
/// Build a Non-Uniform Rational B-Spline.
///
/// If provided, weights must be the same length as the control points. Defaults to equal weights.
@ -570,7 +546,7 @@ impl<P: Point> CubicNurbs<P> {
]
}
}
impl<P: Point> RationalGenerator<P> for CubicNurbs<P> {
impl<P: VectorSpace> RationalGenerator<P> for CubicNurbs<P> {
#[inline]
fn to_curve(&self) -> RationalCurve<P> {
let segments = self
@ -609,10 +585,10 @@ impl<P: Point> RationalGenerator<P> for CubicNurbs<P> {
///
/// ### Continuity
/// The curve is C0 continuous, meaning it has no holes or jumps.
pub struct LinearSpline<P: Point> {
pub struct LinearSpline<P: VectorSpace> {
points: Vec<P>,
}
impl<P: Point> LinearSpline<P> {
impl<P: VectorSpace> LinearSpline<P> {
/// Create a new linear spline
pub fn new(points: impl Into<Vec<P>>) -> Self {
Self {
@ -620,7 +596,7 @@ impl<P: Point> LinearSpline<P> {
}
}
}
impl<P: Point> CubicGenerator<P> for LinearSpline<P> {
impl<P: VectorSpace> CubicGenerator<P> for LinearSpline<P> {
#[inline]
fn to_curve(&self) -> CubicCurve<P> {
let segments = self
@ -639,7 +615,7 @@ impl<P: Point> CubicGenerator<P> for LinearSpline<P> {
}
/// Implement this on cubic splines that can generate a cubic curve from their spline parameters.
pub trait CubicGenerator<P: Point> {
pub trait CubicGenerator<P: VectorSpace> {
/// Build a [`CubicCurve`] by computing the interpolation coefficients for each curve segment.
fn to_curve(&self) -> CubicCurve<P>;
}
@ -649,11 +625,11 @@ pub trait CubicGenerator<P: Point> {
///
/// Segments can be chained together to form a longer compound curve.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CubicSegment<P: Point> {
pub struct CubicSegment<P: VectorSpace> {
coeff: [P; 4],
}
impl<P: Point> CubicSegment<P> {
impl<P: VectorSpace> CubicSegment<P> {
/// Instantaneous position of a point at parametric value `t`.
#[inline]
pub fn position(&self, t: f32) -> P {
@ -807,11 +783,11 @@ impl CubicSegment<Vec2> {
/// Use any struct that implements the [`CubicGenerator`] trait to create a new curve, such as
/// [`CubicBezier`].
#[derive(Clone, Debug, PartialEq)]
pub struct CubicCurve<P: Point> {
pub struct CubicCurve<P: VectorSpace> {
segments: Vec<CubicSegment<P>>,
}
impl<P: Point> CubicCurve<P> {
impl<P: VectorSpace> CubicCurve<P> {
/// Compute the position of a point on the cubic curve at the parametric value `t`.
///
/// Note that `t` varies from `0..=(n_points - 3)`.
@ -912,13 +888,13 @@ impl<P: Point> CubicCurve<P> {
}
}
impl<P: Point> Extend<CubicSegment<P>> for CubicCurve<P> {
impl<P: VectorSpace> Extend<CubicSegment<P>> for CubicCurve<P> {
fn extend<T: IntoIterator<Item = CubicSegment<P>>>(&mut self, iter: T) {
self.segments.extend(iter);
}
}
impl<P: Point> IntoIterator for CubicCurve<P> {
impl<P: VectorSpace> IntoIterator for CubicCurve<P> {
type IntoIter = <Vec<CubicSegment<P>> as IntoIterator>::IntoIter;
type Item = CubicSegment<P>;
@ -929,7 +905,7 @@ impl<P: Point> IntoIterator for CubicCurve<P> {
}
/// Implement this on cubic splines that can generate a rational cubic curve from their spline parameters.
pub trait RationalGenerator<P: Point> {
pub trait RationalGenerator<P: VectorSpace> {
/// Build a [`RationalCurve`] by computing the interpolation coefficients for each curve segment.
fn to_curve(&self) -> RationalCurve<P>;
}
@ -939,7 +915,7 @@ pub trait RationalGenerator<P: Point> {
///
/// Segments can be chained together to form a longer compound curve.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct RationalSegment<P: Point> {
pub struct RationalSegment<P: VectorSpace> {
/// The coefficients matrix of the cubic curve.
coeff: [P; 4],
/// The homogeneous weight coefficients.
@ -948,7 +924,7 @@ pub struct RationalSegment<P: Point> {
knot_span: f32,
}
impl<P: Point> RationalSegment<P> {
impl<P: VectorSpace> RationalSegment<P> {
/// Instantaneous position of a point at parametric value `t` in `[0, knot_span)`.
#[inline]
pub fn position(&self, t: f32) -> P {
@ -1066,11 +1042,11 @@ impl<P: Point> RationalSegment<P> {
/// Use any struct that implements the [`RationalGenerator`] trait to create a new curve, such as
/// [`CubicNurbs`], or convert [`CubicCurve`] using `into/from`.
#[derive(Clone, Debug, PartialEq)]
pub struct RationalCurve<P: Point> {
pub struct RationalCurve<P: VectorSpace> {
segments: Vec<RationalSegment<P>>,
}
impl<P: Point> RationalCurve<P> {
impl<P: VectorSpace> RationalCurve<P> {
/// Compute the position of a point on the curve at the parametric value `t`.
///
/// Note that `t` varies from `0..=(n_points - 3)`.
@ -1190,13 +1166,13 @@ impl<P: Point> RationalCurve<P> {
}
}
impl<P: Point> Extend<RationalSegment<P>> for RationalCurve<P> {
impl<P: VectorSpace> Extend<RationalSegment<P>> for RationalCurve<P> {
fn extend<T: IntoIterator<Item = RationalSegment<P>>>(&mut self, iter: T) {
self.segments.extend(iter);
}
}
impl<P: Point> IntoIterator for RationalCurve<P> {
impl<P: VectorSpace> IntoIterator for RationalCurve<P> {
type IntoIter = <Vec<RationalSegment<P>> as IntoIterator>::IntoIter;
type Item = RationalSegment<P>;
@ -1206,7 +1182,7 @@ impl<P: Point> IntoIterator for RationalCurve<P> {
}
}
impl<P: Point> From<CubicSegment<P>> for RationalSegment<P> {
impl<P: VectorSpace> From<CubicSegment<P>> for RationalSegment<P> {
fn from(value: CubicSegment<P>) -> Self {
Self {
coeff: value.coeff,
@ -1216,7 +1192,7 @@ impl<P: Point> From<CubicSegment<P>> for RationalSegment<P> {
}
}
impl<P: Point> From<CubicCurve<P>> for RationalCurve<P> {
impl<P: VectorSpace> From<CubicCurve<P>> for RationalCurve<P> {
fn from(value: CubicCurve<P>) -> Self {
Self {
segments: value.segments.into_iter().map(Into::into).collect(),

View file

@ -14,6 +14,7 @@
mod affine3;
mod aspect_ratio;
pub mod bounding;
mod common_traits;
pub mod cubic_splines;
mod direction;
mod float_ord;
@ -26,6 +27,7 @@ mod shape_sampling;
pub use affine3::*;
pub use aspect_ratio::AspectRatio;
pub use common_traits::*;
pub use direction::*;
pub use float_ord::*;
pub use ray::{Ray2d, Ray3d};

View file

@ -1,6 +1,6 @@
use std::f32::consts::{PI, TAU};
use crate::{primitives::*, Vec2, Vec3};
use crate::{primitives::*, NormedVectorSpace, Vec2, Vec3};
use rand::{
distributions::{Distribution, WeightedIndex},
Rng,
@ -142,6 +142,79 @@ impl ShapeSample for Cuboid {
}
}
/// Interior sampling for triangles which doesn't depend on the ambient dimension.
fn sample_triangle_interior<P: NormedVectorSpace, R: Rng + ?Sized>(
vertices: [P; 3],
rng: &mut R,
) -> P {
let [a, b, c] = vertices;
let ab = b - a;
let ac = c - a;
// Generate random points on a parallelipiped and reflect so that
// we can use the points that lie outside the triangle
let u = rng.gen_range(0.0..=1.0);
let v = rng.gen_range(0.0..=1.0);
if u + v > 1. {
let u1 = 1. - v;
let v1 = 1. - u;
ab * u1 + ac * v1
} else {
ab * u + ac * v
}
}
/// Boundary sampling for triangles which doesn't depend on the ambient dimension.
fn sample_triangle_boundary<P: NormedVectorSpace, R: Rng + ?Sized>(
vertices: [P; 3],
rng: &mut R,
) -> P {
let [a, b, c] = vertices;
let ab = b - a;
let ac = c - a;
let bc = c - b;
let t = rng.gen_range(0.0..=1.0);
if let Ok(dist) = WeightedIndex::new([ab.norm(), ac.norm(), bc.norm()]) {
match dist.sample(rng) {
0 => a.lerp(b, t),
1 => a.lerp(c, t),
2 => b.lerp(c, t),
_ => unreachable!(),
}
} else {
// This should only occur when the triangle is 0-dimensional degenerate
// so this is actually the correct result.
a
}
}
impl ShapeSample for Triangle2d {
type Output = Vec2;
fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
sample_triangle_interior(self.vertices, rng)
}
fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
sample_triangle_boundary(self.vertices, rng)
}
}
impl ShapeSample for Triangle3d {
type Output = Vec3;
fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
sample_triangle_interior(self.vertices, rng)
}
fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
sample_triangle_boundary(self.vertices, rng)
}
}
impl ShapeSample for Cylinder {
type Output = Vec3;

View file

@ -1,10 +1,10 @@
//! Demonstrates how to animate colors in different color spaces using mixing and splines.
use bevy::{math::cubic_splines::Point, prelude::*};
use bevy::{math::VectorSpace, prelude::*};
// We define this trait so we can reuse the same code for multiple color types that may be implemented using curves.
trait CurveColor: Point + Into<Color> + Send + Sync + 'static {}
impl<T: Point + Into<Color> + Send + Sync + 'static> CurveColor for T {}
trait CurveColor: VectorSpace + Into<Color> + Send + Sync + 'static {}
impl<T: VectorSpace + Into<Color> + Send + Sync + 'static> CurveColor for T {}
// We define this trait so we can reuse the same code for multiple color types that may be implemented using mixing.
trait MixedColor: Mix + Into<Color> + Send + Sync + 'static {}