Disallow empty cubic and rational curves (#14382)

# Objective

Previously, our cubic spline constructors would produce
`CubicCurve`/`RationalCurve` output with no data when they themselves
didn't hold enough control points to produce a well-formed curve.
Attempting to sample the resulting empty "curves" (e.g. by calling
`CubicCurve::position`) would crash the program (😓).

The objectives of this PR are: 
1. Ensure that the curve output of `bevy_math`'s spline constructions
are never invalid as data.
2. Provide a type-level guarantee that `CubicCurve` and `RationalCurve`
actually function as curves.

## Solution

This has a few pieces. Firstly, the curve generator traits
`CubicGenerator`, `CyclicCubicGenerator`, and `RationalGenerator` are
now fallible — they have associated error types, and the
curve-generation functions are allowed to fail:
```rust
/// Implement this on cubic splines that can generate a cubic curve from their spline parameters.
pub trait CubicGenerator<P: VectorSpace> {
    /// An error type indicating why construction might fail.
    type Error;

    /// Build a [`CubicCurve`] by computing the interpolation coefficients for each curve segment.
    fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error>;
}
```

All existing spline constructions use this together with errors that
indicate when they didn't have the right control data and provide curves
which have at least one segment whenever they return an `Ok` variant.

Next, `CubicCurve` and `RationalCurve` have been blessed with a
guarantee that their internal array of segments (`segments`) is never
empty. In particular, this field is no longer public, so that invalid
curves cannot be built using struct instantiation syntax. To compensate
for this shortfall for users (in particular library authors who might
want to implement their own generators), there is a new method
`from_segments` on these for constructing a curve from a list of
segments, failing if the list is empty:
```rust
/// Create a new curve from a collection of segments. If the collection of segments is empty,
/// a curve cannot be built and `None` will be returned instead.
pub fn from_segments(segments: impl Into<Vec<CubicSegment<P>>>) -> Option<Self> { //... }
```

All existing methods on `CyclicCurve` and `CubicCurve` maintain the
invariant, so the direct construction of invalid values by users is
impossible.

## Testing

Run unit tests from `bevy_math::cubic_splines`. Additionally, run the
`cubic_splines` example and try to get it to crash using small numbers
of control points: it uses the fallible constructors directly, so if
invalid data is ever constructed, it is basically guaranteed to crash.

---

## Migration Guide

The `to_curve` method on Bevy's cubic splines is now fallible (returning
a `Result`), meaning that any existing calls will need to be updated by
handling the possibility of an error variant.

Similarly, any custom implementation of `CubicGenerator` or
`RationalGenerator` will need to be amended to include an `Error` type
and be made fallible itself.

Finally, the fields of `CubicCurve` and `RationalCurve` are now private,
so any direct constructions of these structs from segments will need to
be replaced with the new `CubicCurve::from_segments` and
`RationalCurve::from_segments` methods.

---

## Design

The main thing to justify here is the choice for the curve internals to
remain the same. After all, if they were able to cause crashes in the
first place, it's worth wondering why safeguards weren't put in place on
the types themselves to prevent that.

My view on this is that the problem was really that the internals of
these methods implicitly relied on the assumption that the value they
were operating on was *actually a curve*, when this wasn't actually
guaranteed. Now, it's possible to make a bunch of small changes inside
the curve struct methods to account for that, but I think that's worse
than just guaranteeing that the data is valid upstream — sampling is
about as hot a code path as we're going to get in this area, and hitting
an additional branch every time it happens just to check that the struct
contains valid data is probably a waste of resources.

Another way of phrasing this is that even if we're only interested in
solving the crashes, the curve's validity needs to be checked at some
point, and it's almost certainly better to do this once at the point of
construction than every time the curve is sampled.

In cases where the control data is supplied dynamically, users would
already have to deal with empty curve outputs basically not working.
Anecdotally, I ran into this while writing the `cubic_splines` example,
and I think the diff illustrates the improvement pretty nicely — the
code no longer has to anticipate whether the output will be good or not;
it just has to handle the `Result`.

The cost of all this, of course, is that we have to guarantee that the
new invariant is actually maintained whenever we extend the API.
However, for the most part, I don't expect users to want to do much
surgery on the internals of their curves anyway.
This commit is contained in:
Matty 2024-07-29 19:25:14 -04:00 committed by GitHub
parent 7de271f992
commit 74cecb27bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 206 additions and 92 deletions

View file

@ -42,6 +42,9 @@ pub struct AutoExposureCompensationCurve {
/// Various errors that can occur when constructing an [`AutoExposureCompensationCurve`].
#[derive(Error, Debug)]
pub enum AutoExposureCompensationCurveError {
/// The curve couldn't be built in the first place.
#[error("curve could not be constructed from the given data")]
InvalidCurve,
/// A discontinuity was found in the curve.
#[error("discontinuity found between curve segments")]
DiscontinuityFound,
@ -99,7 +102,9 @@ impl AutoExposureCompensationCurve {
where
T: CubicGenerator<Vec2>,
{
let curve = curve.to_curve();
let Ok(curve) = curve.to_curve() else {
return Err(AutoExposureCompensationCurveError::InvalidCurve);
};
let min_log_lum = curve.position(0.0).x;
let max_log_lum = curve.position(curve.segments().len() as f32).x;

View file

@ -41,7 +41,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
/// vec2(5.0, 3.0),
/// vec2(9.0, 8.0),
/// ]];
/// let bezier = CubicBezier::new(points).to_curve();
/// let bezier = CubicBezier::new(points).to_curve().unwrap();
/// let positions: Vec<_> = bezier.iter_positions(100).collect();
/// ```
#[derive(Clone, Debug)]
@ -60,8 +60,10 @@ impl<P: VectorSpace> CubicBezier<P> {
}
}
impl<P: VectorSpace> CubicGenerator<P> for CubicBezier<P> {
type Error = CubicBezierError;
#[inline]
fn to_curve(&self) -> CubicCurve<P> {
fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
// A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin.
// <https://xiaoxingchen.github.io/2020/03/02/bspline_in_so3/general_matrix_representation_for_bsplines.pdf>
// See section 4.2 and equation 11.
@ -76,12 +78,22 @@ impl<P: VectorSpace> CubicGenerator<P> for CubicBezier<P> {
.control_points
.iter()
.map(|p| CubicSegment::coefficients(*p, char_matrix))
.collect();
.collect_vec();
CubicCurve { segments }
if segments.is_empty() {
Err(CubicBezierError)
} else {
Ok(CubicCurve { segments })
}
}
}
/// 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)]
#[error("Unable to generate cubic curve: at least one set of control points is required")]
pub struct CubicBezierError;
/// A spline interpolated continuously between the nearest two control points, with the position and
/// velocity of the curve specified at both control points. This curve passes through all control
/// points, with the specified velocity which includes direction and parametric speed.
@ -120,7 +132,7 @@ impl<P: VectorSpace> CubicGenerator<P> for CubicBezier<P> {
/// vec2(0.0, 1.0),
/// vec2(0.0, 1.0),
/// ];
/// let hermite = CubicHermite::new(points, tangents).to_curve();
/// let hermite = CubicHermite::new(points, tangents).to_curve().unwrap();
/// let positions: Vec<_> = hermite.iter_positions(100).collect();
/// ```
///
@ -158,8 +170,10 @@ impl<P: VectorSpace> CubicHermite<P> {
}
}
impl<P: VectorSpace> CubicGenerator<P> for CubicHermite<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve(&self) -> CubicCurve<P> {
fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
let segments = self
.control_points
.windows(2)
@ -167,14 +181,23 @@ impl<P: VectorSpace> CubicGenerator<P> for CubicHermite<P> {
let (p0, v0, p1, v1) = (p[0].0, p[0].1, p[1].0, p[1].1);
CubicSegment::coefficients([p0, v0, p1, v1], self.char_matrix())
})
.collect();
.collect_vec();
CubicCurve { segments }
if segments.is_empty() {
Err(InsufficientDataError {
expected: 2,
given: self.control_points.len(),
})
} else {
Ok(CubicCurve { segments })
}
}
}
impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicHermite<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve_cyclic(&self) -> CubicCurve<P> {
fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
let segments = self
.control_points
.iter()
@ -183,9 +206,16 @@ impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicHermite<P> {
let (p0, v0, p1, v1) = (j0.0, j0.1, j1.0, j1.1);
CubicSegment::coefficients([p0, v0, p1, v1], self.char_matrix())
})
.collect();
.collect_vec();
CubicCurve { segments }
if segments.is_empty() {
Err(InsufficientDataError {
expected: 2,
given: self.control_points.len(),
})
} else {
Ok(CubicCurve { segments })
}
}
}
@ -220,7 +250,7 @@ impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicHermite<P> {
/// vec2(5.0, 3.0),
/// vec2(9.0, 8.0),
/// ];
/// let cardinal = CubicCardinalSpline::new(0.3, points).to_curve();
/// let cardinal = CubicCardinalSpline::new(0.3, points).to_curve().unwrap();
/// let positions: Vec<_> = cardinal.iter_positions(100).collect();
/// ```
///
@ -267,13 +297,18 @@ impl<P: VectorSpace> CubicCardinalSpline<P> {
}
}
impl<P: VectorSpace> CubicGenerator<P> for CubicCardinalSpline<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve(&self) -> CubicCurve<P> {
fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
let length = self.control_points.len();
// Early return to avoid accessing an invalid index
if length < 2 {
return CubicCurve { segments: vec![] };
return Err(InsufficientDataError {
expected: 2,
given: self.control_points.len(),
});
}
// Extend the list of control points by mirroring the last second-to-last control points on each end;
@ -292,18 +327,23 @@ impl<P: VectorSpace> CubicGenerator<P> for CubicCardinalSpline<P> {
.map(|(&p0, &p1, &p2, &p3)| {
CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix())
})
.collect();
.collect_vec();
CubicCurve { segments }
Ok(CubicCurve { segments })
}
}
impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicCardinalSpline<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve_cyclic(&self) -> CubicCurve<P> {
fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
let len = self.control_points.len();
if len == 0 {
return CubicCurve { segments: vec![] };
if len < 2 {
return Err(InsufficientDataError {
expected: 2,
given: self.control_points.len(),
});
}
// This would ordinarily be the last segment, but we pick it out so that we can make it first
@ -331,7 +371,7 @@ impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicCardinalSpline<P> {
segments.push(first_segment);
segments.extend(later_segments);
CubicCurve { segments }
Ok(CubicCurve { segments })
}
}
@ -363,7 +403,7 @@ impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicCardinalSpline<P> {
/// vec2(5.0, 3.0),
/// vec2(9.0, 8.0),
/// ];
/// let b_spline = CubicBSpline::new(points).to_curve();
/// let b_spline = CubicBSpline::new(points).to_curve().unwrap();
/// let positions: Vec<_> = b_spline.iter_positions(100).collect();
/// ```
///
@ -406,34 +446,52 @@ impl<P: VectorSpace> CubicBSpline<P> {
}
}
impl<P: VectorSpace> CubicGenerator<P> for CubicBSpline<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve(&self) -> CubicCurve<P> {
fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
let segments = self
.control_points
.windows(4)
.map(|p| CubicSegment::coefficients([p[0], p[1], p[2], p[3]], self.char_matrix()))
.collect();
.collect_vec();
CubicCurve { segments }
if segments.is_empty() {
Err(InsufficientDataError {
expected: 4,
given: self.control_points.len(),
})
} else {
Ok(CubicCurve { segments })
}
}
}
impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicBSpline<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve_cyclic(&self) -> CubicCurve<P> {
fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
let segments = self
.control_points
.iter()
.circular_tuple_windows()
.map(|(&a, &b, &c, &d)| CubicSegment::coefficients([a, b, c, d], self.char_matrix()))
.collect();
.collect_vec();
// Note that the parametrization is consistent with the one for `to_curve` but with
// the extra curve segments all tacked on at the end. This might be slightly counter-intuitive,
// since it means the first segment doesn't go "between" the first two control points, but
// between the second and third instead.
CubicCurve { segments }
if segments.is_empty() {
Err(InsufficientDataError {
expected: 2,
given: self.control_points.len(),
})
} else {
Ok(CubicCurve { segments })
}
}
}
@ -513,7 +571,8 @@ pub enum CubicNurbsError {
/// let knots = [0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0];
/// let nurbs = CubicNurbs::new(points, Some(weights), Some(knots))
/// .expect("NURBS construction failed!")
/// .to_curve();
/// .to_curve()
/// .unwrap();
/// let positions: Vec<_> = nurbs.iter_positions(100).collect();
/// ```
#[derive(Clone, Debug)]
@ -687,8 +746,10 @@ impl<P: VectorSpace> CubicNurbs<P> {
}
}
impl<P: VectorSpace> RationalGenerator<P> for CubicNurbs<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve(&self) -> RationalCurve<P> {
fn to_curve(&self) -> Result<RationalCurve<P>, Self::Error> {
let segments = self
.control_points
.windows(4)
@ -710,8 +771,15 @@ impl<P: VectorSpace> RationalGenerator<P> for CubicNurbs<P> {
matrix,
)
})
.collect();
RationalCurve { segments }
.collect_vec();
if segments.is_empty() {
Err(InsufficientDataError {
expected: 4,
given: self.control_points.len(),
})
} else {
Ok(RationalCurve { segments })
}
}
}
@ -746,8 +814,10 @@ impl<P: VectorSpace> LinearSpline<P> {
}
}
impl<P: VectorSpace> CubicGenerator<P> for LinearSpline<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve(&self) -> CubicCurve<P> {
fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error> {
let segments = self
.points
.windows(2)
@ -758,13 +828,23 @@ impl<P: VectorSpace> CubicGenerator<P> for LinearSpline<P> {
coeff: [a, b - a, P::default(), P::default()],
}
})
.collect();
CubicCurve { segments }
.collect_vec();
if segments.is_empty() {
Err(InsufficientDataError {
expected: 2,
given: self.points.len(),
})
} else {
Ok(CubicCurve { segments })
}
}
}
impl<P: VectorSpace> CyclicCubicGenerator<P> for LinearSpline<P> {
type Error = InsufficientDataError;
#[inline]
fn to_curve_cyclic(&self) -> CubicCurve<P> {
fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error> {
let segments = self
.points
.iter()
@ -772,24 +852,46 @@ impl<P: VectorSpace> CyclicCubicGenerator<P> for LinearSpline<P> {
.map(|(&a, &b)| CubicSegment {
coeff: [a, b - a, P::default(), P::default()],
})
.collect();
CubicCurve { segments }
.collect_vec();
if segments.is_empty() {
Err(InsufficientDataError {
expected: 2,
given: self.points.len(),
})
} else {
Ok(CubicCurve { segments })
}
}
}
/// An error indicating that a spline construction didn't have enough control points to generate a curve.
#[derive(Clone, Debug, Error)]
#[error("Not enough data to build curve: needed at least {expected} control points but was only given {given}")]
pub struct InsufficientDataError {
expected: usize,
given: usize,
}
/// Implement this on cubic splines that can generate a cubic curve from their spline parameters.
pub trait CubicGenerator<P: VectorSpace> {
/// An error type indicating why construction might fail.
type Error;
/// Build a [`CubicCurve`] by computing the interpolation coefficients for each curve segment.
fn to_curve(&self) -> CubicCurve<P>;
fn to_curve(&self) -> Result<CubicCurve<P>, Self::Error>;
}
/// Implement this on cubic splines that can generate a cyclic cubic curve from their spline parameters.
///
/// This makes sense only when the control data can be interpreted cyclically.
pub trait CyclicCubicGenerator<P: VectorSpace> {
/// An error type indicating why construction might fail.
type Error;
/// Build a cyclic [`CubicCurve`] by computing the interpolation coefficients for each curve segment,
/// treating the control data as cyclic so that the result is a closed curve.
fn to_curve_cyclic(&self) -> CubicCurve<P>;
fn to_curve_cyclic(&self) -> Result<CubicCurve<P>, Self::Error>;
}
/// A segment of a cubic curve, used to hold precomputed coefficients for fast interpolation.
@ -858,7 +960,9 @@ impl CubicSegment<Vec2> {
/// example, the ubiquitous "ease-in-out" is defined as `(0.25, 0.1), (0.25, 1.0)`.
pub fn new_bezier(p1: impl Into<Vec2>, p2: impl Into<Vec2>) -> Self {
let (p0, p3) = (Vec2::ZERO, Vec2::ONE);
let bezier = CubicBezier::new([[p0, p1.into(), p2.into(), p3]]).to_curve();
let bezier = CubicBezier::new([[p0, p1.into(), p2.into(), p3]])
.to_curve()
.unwrap(); // Succeeds because resulting curve is guaranteed to have one segment
bezier.segments[0]
}
@ -959,11 +1063,22 @@ impl CubicSegment<Vec2> {
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicCurve<P: VectorSpace> {
/// Segments of the curve
pub segments: Vec<CubicSegment<P>>,
/// The segments comprising the curve. This must always be nonempty.
segments: Vec<CubicSegment<P>>,
}
impl<P: VectorSpace> CubicCurve<P> {
/// Create a new curve from a collection of segments. If the collection of segments is empty,
/// a curve cannot be built and `None` will be returned instead.
pub fn from_segments(segments: impl Into<Vec<CubicSegment<P>>>) -> Option<Self> {
let segments: Vec<_> = segments.into();
if segments.is_empty() {
None
} else {
Some(Self { segments })
}
}
/// Compute the position of a point on the cubic curve at the parametric value `t`.
///
/// Note that `t` varies from `0..=(n_points - 3)`.
@ -1082,8 +1197,11 @@ impl<P: VectorSpace> IntoIterator for CubicCurve<P> {
/// Implement this on cubic splines that can generate a rational cubic curve from their spline parameters.
pub trait RationalGenerator<P: VectorSpace> {
/// An error type indicating why construction might fail.
type Error;
/// Build a [`RationalCurve`] by computing the interpolation coefficients for each curve segment.
fn to_curve(&self) -> RationalCurve<P>;
fn to_curve(&self) -> Result<RationalCurve<P>, Self::Error>;
}
/// A segment of a rational cubic curve, used to hold precomputed coefficients for fast interpolation.
@ -1221,11 +1339,22 @@ impl<P: VectorSpace> RationalSegment<P> {
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct RationalCurve<P: VectorSpace> {
/// The segments in the curve
pub segments: Vec<RationalSegment<P>>,
/// The segments comprising the curve. This must always be nonempty.
segments: Vec<RationalSegment<P>>,
}
impl<P: VectorSpace> RationalCurve<P> {
/// Create a new curve from a collection of segments. If the collection of segments is empty,
/// a curve cannot be built and `None` will be returned instead.
pub fn from_segments(segments: impl Into<Vec<RationalSegment<P>>>) -> Option<Self> {
let segments: Vec<_> = segments.into();
if segments.is_empty() {
None
} else {
Some(Self { segments })
}
}
/// Compute the position of a point on the curve at the parametric value `t`.
///
/// Note that `t` varies from `0..=(n_points - 3)`.
@ -1401,7 +1530,7 @@ mod tests {
vec2(5.0, 3.0),
vec2(9.0, 8.0),
]];
let bezier = CubicBezier::new(points).to_curve();
let bezier = CubicBezier::new(points).to_curve().unwrap();
for i in 0..=N_SAMPLES {
let t = i as f32 / N_SAMPLES as f32; // Check along entire length
assert!(bezier.position(t).distance(cubic_manual(t, points[0])) <= FLOAT_EQ);
@ -1458,7 +1587,9 @@ mod tests {
let tension = 0.2;
let [p0, p1, p2, p3] = [vec2(-1., -2.), vec2(0., 1.), vec2(1., 2.), vec2(-2., 1.)];
let curve = CubicCardinalSpline::new(tension, [p0, p1, p2, p3]).to_curve();
let curve = CubicCardinalSpline::new(tension, [p0, p1, p2, p3])
.to_curve()
.unwrap();
// Positions at segment endpoints
assert!(curve.position(0.).abs_diff_eq(p0, FLOAT_EQ));
@ -1496,7 +1627,7 @@ mod tests {
vec2(0.0, 0.0),
];
let b_spline = CubicBSpline::new(points).to_curve();
let b_spline = CubicBSpline::new(points).to_curve().unwrap();
let rational_b_spline = RationalCurve::from(b_spline.clone());
/// Tests if two vectors of points are approximately the same
@ -1554,7 +1685,7 @@ mod tests {
let weights = [1.0, weight, weight, 1.0];
let knots = [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];
let spline = CubicNurbs::new(points, Some(weights), Some(knots)).unwrap();
let curve = spline.to_curve();
let curve = spline.to_curve().unwrap();
for (i, point) in curve.iter_positions(10).enumerate() {
assert!(
f32::abs(point.length() - 1.0) < EPSILON,

View file

@ -78,7 +78,7 @@ fn spawn_curve_sprite<T: CurveColor>(commands: &mut Commands, y: f32, points: [T
},
..Default::default()
},
Curve(CubicBezier::new([points]).to_curve()),
Curve(CubicBezier::new([points]).to_curve().unwrap()),
));
}

View file

@ -34,7 +34,7 @@ fn setup(
]];
// Make a CubicCurve
let bezier = CubicBezier::new(points).to_curve();
let bezier = CubicBezier::new(points).to_curve().unwrap();
// Spawning a cube to experiment on
commands.spawn((

View file

@ -147,15 +147,7 @@ impl std::fmt::Display for CyclingMode {
/// The curve presently being displayed. This is optional because there may not be enough control
/// points to actually generate a curve.
#[derive(Clone, Default, Resource)]
struct Curve {
inner: Option<CubicCurve<Vec2>>,
}
impl From<CubicCurve<Vec2>> for Curve {
fn from(value: CubicCurve<Vec2>) -> Self {
Self { inner: Some(value) }
}
}
struct Curve(Option<CubicCurve<Vec2>>);
/// The control points used to generate a curve. The tangent components are only used in the case of
/// Hermite interpolation.
@ -184,11 +176,11 @@ fn update_curve(
/// This system uses gizmos to draw the current [`Curve`] by breaking it up into a large number
/// of line segments.
fn draw_curve(curve: Res<Curve>, mut gizmos: Gizmos) {
let Some(ref curve) = curve.inner else {
let Some(ref curve) = curve.0 else {
return;
};
// Scale resolution with curve length so it doesn't degrade as the length increases.
let resolution = 100 * curve.segments.len();
let resolution = 100 * curve.segments().len();
gizmos.linestrip(
curve.iter_positions(resolution).map(|pt| pt.extend(0.0)),
Color::srgb(1.0, 1.0, 1.0),
@ -226,41 +218,27 @@ fn form_curve(
match spline_mode {
SplineMode::Hermite => {
if points.len() < 2 {
Curve::default()
} else {
let spline = CubicHermite::new(points, tangents);
Curve::from(match cycling_mode {
CyclingMode::NotCyclic => spline.to_curve(),
CyclingMode::Cyclic => spline.to_curve_cyclic(),
Curve(match cycling_mode {
CyclingMode::NotCyclic => spline.to_curve().ok(),
CyclingMode::Cyclic => spline.to_curve_cyclic().ok(),
})
}
}
SplineMode::Cardinal => {
if points.len() < 2 {
Curve::default()
} else {
let spline = CubicCardinalSpline::new_catmull_rom(points);
Curve::from(match cycling_mode {
CyclingMode::NotCyclic => spline.to_curve(),
CyclingMode::Cyclic => spline.to_curve_cyclic(),
Curve(match cycling_mode {
CyclingMode::NotCyclic => spline.to_curve().ok(),
CyclingMode::Cyclic => spline.to_curve_cyclic().ok(),
})
}
}
SplineMode::B => {
if matches!(cycling_mode, CyclingMode::NotCyclic) && points.len() < 4
|| matches!(cycling_mode, CyclingMode::Cyclic) && points.len() < 2
{
Curve::default()
} else {
let spline = CubicBSpline::new(points);
Curve::from(match cycling_mode {
CyclingMode::NotCyclic => spline.to_curve(),
CyclingMode::Cyclic => spline.to_curve_cyclic(),
Curve(match cycling_mode {
CyclingMode::NotCyclic => spline.to_curve().ok(),
CyclingMode::Cyclic => spline.to_curve_cyclic().ok(),
})
}
}
}
}
// --------------------