Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make cardinal splines include endpoints (bevyengine#12574)
# Objective - Fixes bevyengine#12570 ## Solution Previously, cardinal splines constructed by `CubicCardinalSpline` would leave out their endpoints when constructing the cubic curve segments connecting their points. (See the linked issue for details.) Now, cardinal splines include the endpoints. For instance, the provided usage example ```rust let points = [ vec2(-1.0, -20.0), vec2(3.0, 2.0), vec2(5.0, 3.0), vec2(9.0, 8.0), ]; let cardinal = CubicCardinalSpline::new(0.3, points).to_curve(); let positions: Vec<_> = cardinal.iter_positions(100).collect(); ``` will actually produce a spline that connects all four of these points instead of just the middle two "interior" points. Internally, this is achieved by duplicating the endpoints of the vector of control points before performing the construction of the associated `CubicCurve`. This amounts to specifying that the tangents at the endpoints `P_0` and `P_n` (say) should be parallel to `P_1 - P_0` and `P_n - P_{n-1}`. --- ## Migration Guide Any users relying on the old behavior of `CubicCardinalSpline` will have to truncate any parametrizations they used in order to access a curve identical to the one they had previously. This would be done by chopping off a unit-distance segment from each end of the parametrizing interval. For instance, if a user's existing code looks as follows ```rust fn interpolate(t: f32) -> Vec2 { let points = [ vec2(-1.0, -20.0), vec2(3.0, 2.0), vec2(5.0, 3.0), vec2(9.0, 8.0), ]; let my_curve = CubicCardinalSpline::new(0.3, points).to_curve(); my_curve.position(t) } ``` then in order to obtain similar behavior, `t` will need to be shifted up by 1, since the output of `CubicCardinalSpline::to_curve` has introduced a new segment in the interval [0,1], displacing the old segment from [0,1] to [1,2]: ```rust fn interpolate(t: f32) -> Vec2 { let points = [ vec2(-1.0, -20.0), vec2(3.0, 2.0), vec2(5.0, 3.0), vec2(9.0, 8.0), ]; let my_curve = CubicCardinalSpline::new(0.3, points).to_curve(); my_curve.position(t+1) } ``` (Note that this does not provide identical output for values of `t` outside of the interval [0,1].) On the other hand, any user who was specifying additional endpoint tangents simply to get the curve to pass through the right points (i.e. not requiring exactly the same output) can simply omit the endpoints that were being supplied only for control purposes. --- ## Discussion ### Design considerations This is one of the two approaches outlined in bevyengine#12570 — in this PR, we are basically declaring that the docs are right and the implementation was flawed. One semi-interesting question is how the endpoint tangents actually ought to be defined when we include them, and another option considered was mirroring the control points adjacent to the endpoints instead of duplicating them, which would have had the advantage that the expected length of the corresponding difference should be more similar to that of the other difference-tangents, provided that the points are equally spaced. In this PR, the duplication method (which produces smaller tangents) was chosen for a couple reasons: - It seems to be more standard - It is exceptionally simple to implement - I was a little concerned that the aforementioned alternative would result in some over-extrapolation ### An annoyance If you look at the code, you'll see I was unable to find a satisfactory way of doing this without allocating a new vector. This doesn't seem like a big problem given the context, but it does bother me. In particular, if there is some easy parallel to `slice::windows` for iterators that doesn't pull in an external dependency, I would love to know about it.
- Loading branch information