From cb7dcb73f70fb0484b123fa943fe292cd2208c64 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 11 Apr 2024 15:51:13 -0400 Subject: [PATCH 01/22] Initial commit --- rfcs/curve-trait.md | 82 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 rfcs/curve-trait.md diff --git a/rfcs/curve-trait.md b/rfcs/curve-trait.md new file mode 100644 index 00000000..c67324df --- /dev/null +++ b/rfcs/curve-trait.md @@ -0,0 +1,82 @@ +# Feature Name: `curve-trait` + +## Summary + +One paragraph explanation of the feature. + +## Motivation + +Why are we doing this? What use cases does it support? + +## User-facing explanation + +Explain the proposal as if it was already included in the engine and you were teaching it to another Bevy user. That generally means: + +- Introducing new named concepts. +- Explaining the feature, ideally through simple examples of solutions to concrete problems. +- Explaining how Bevy users should *think* about the feature, and how it should impact the way they use Bevy. It should explain the impact as concretely as possible. +- If applicable, provide sample error messages, deprecation warnings, or migration guidance. +- If applicable, explain how this feature compares to similar existing features, and in what situations the user would use each one. + +## Implementation strategy + +This is the technical portion of the RFC. +Try to capture the broad implementation strategy, +and then focus in on the tricky details so that: + +- Its interaction with other features is clear. +- It is reasonably clear how the feature would be implemented. +- Corner cases are dissected by example. + +When necessary, this section should return to the examples given in the previous section and explain the implementation details that make them work. + +When writing this section be mindful of the following [repo guidelines](https://github.com/bevyengine/rfcs): + +- **RFCs should be scoped:** Try to avoid creating RFCs for huge design spaces that span many features. Try to pick a specific feature slice and describe it in as much detail as possible. Feel free to create multiple RFCs if you need multiple features. +- **RFCs should avoid ambiguity:** Two developers implementing the same RFC should come up with nearly identical implementations. +- **RFCs should be "implementable":** Merged RFCs should only depend on features from other merged RFCs and existing Bevy features. It is ok to create multiple dependent RFCs, but they should either be merged at the same time or have a clear merge order that ensures the "implementable" rule is respected. + +## Drawbacks + +Why should we *not* do this? + +## Rationale and alternatives + +- Why is this design the best in the space of possible designs? +- What other designs have been considered and what is the rationale for not choosing them? +- What objections immediately spring to mind? How have you addressed them? +- What is the impact of not doing this? +- Why is this important to implement as a feature of Bevy itself, rather than an ecosystem crate? + +## \[Optional\] Prior art + +Discuss prior art, both the good and the bad, in relation to this proposal. +This can include: + +- Does this feature exist in other libraries and what experiences have their community had? +- Papers: Are there any published papers or great posts that discuss this? + +This section is intended to encourage you as an author to think about the lessons from other tools and provide readers of your RFC with a fuller picture. + +Note that while precedent set by other engines is some motivation, it does not on its own motivate an RFC. + +## Unresolved questions + +- What parts of the design do you expect to resolve through the RFC process before this gets merged? +- What parts of the design do you expect to resolve through the implementation of this feature before the feature PR is merged? +- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? + +## \[Optional\] Future possibilities + +Think about what the natural extension and evolution of your proposal would +be and how it would affect Bevy as a whole in a holistic way. +Try to use this section as a tool to more fully consider other possible +interactions with the engine in your proposal. + +This is also a good place to "dump ideas", if they are out of scope for the +RFC you are writing but otherwise related. + +Note that having something written down in the future-possibilities section +is not a reason to accept the current or a future RFC; such notes should be +in the section on motivation or rationale in this or subsequent RFCs. +If a feature or change has no direct value on its own, expand your RFC to include the first valuable feature that would build on it. From eb6dbaf5ed87f233db87e23c2986e049a01c4eeb Mon Sep 17 00:00:00 2001 From: Matty Date: Thu, 11 Apr 2024 19:38:27 -0400 Subject: [PATCH 02/22] Write curve-trait.md --- rfcs/curve-trait.md | 450 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 402 insertions(+), 48 deletions(-) diff --git a/rfcs/curve-trait.md b/rfcs/curve-trait.md index c67324df..00eb0727 100644 --- a/rfcs/curve-trait.md +++ b/rfcs/curve-trait.md @@ -1,82 +1,436 @@ -# Feature Name: `curve-trait` +Feature Name: `curve-trait` ## Summary -One paragraph explanation of the feature. +This RFC introduces a general trait API, `Curve`, for shared functionality of curves within Bevy's +ecosystem. This encompasses both *abstract* curves, such as those produced by splines (and hence defined +by equations), as well as *concrete* curves that are defined by sampling and interpolation (e.g. +animation keyframes), among others. + +The purpose is to provide a common baseline of useful functionality that unites these disparate +implementations, making curves of many kinds pleasant to work with for authors of plugins and games. ## Motivation -Why are we doing this? What use cases does it support? +Curves tend to have quite disparate underlying implementations at the level of data. As a result, +they are prone to causing ecosystem fragmentation, as authors of various plugins (including internal ones) +are liable to create the curve abstractions that suit their own particular needs. + +By providing a common baseline in `Curve`, we can ensure a significant degree of portability between these +disparate areas and, ideally, allow for authors to reuse the work of others rather than reinventing the +wheel every time they want to write libraries that involve generating or working with curves. + +Furthermore, something like this is also a prerequisite to bringing more complex geometric operations +(some of them with broad implications) into `bevy_math` itself. For instance, this lays the natural bedwork +for more specialized libraries in curve geometry, such as those needed by curve extrusion (e.g. for mesh +generation of roads and other surfaces). Without an internal curve abstraction in Bevy itself, we would +be forced to repeat our work for each class of curves that we want to work with. ## User-facing explanation -Explain the proposal as if it was already included in the engine and you were teaching it to another Bevy user. That generally means: +### Introduction + +The trait `Curve` provides a generalized API surface for curves. Its requirements are quite simple: +```rust +pub trait Curve +where + T: Interpolable, +{ + fn duration(&self) -> f32; + fn sample(&self, t: f32) -> T; +} +``` +At a basic level, it encompasses values of type `T` parametrized over an interval that starts at 0 and ends +at the curve's given `duration`, allowing that data to be sampled by providing the associated time parameter. + +### Interpolation + +The only requirement of `T` to be used with `Curve` is that it implements the `Interpolable` trait, so we +will briefly examine that next: + +```rust +pub trait Interpolable: Clone { + fn interpolate(&self, other: &Self, t: f32) -> Self; +} +``` + +As you can see, in addition to the `Clone` requirement, `Interpolable` requires a single method, `interpolate`, +which takes two items by reference and produces a third by interpolating between them. The idea is that +when `t` is 0, `self` is recovered, whereas when `t` is 1, you receive `other`. Intermediate values are to be +obtained by an interpolation process that is provided by the implementation. + +(Intuitively, `Clone` is required by `Interpolable` because at the endpoints 0 and 1, clones of the starting and +ending points should be returned. Frequently, `Interpolable` data will actually be `Copy`.) + +For example, Bevy's vector types (including `f32`, `Vec2`, `Vec3`, etc.) implement `Interpolable` using linear +interpolation (the `lerp` function here): +```rust +impl Interpolable for T +where + T: VectorSpace, +{ + fn interpolate(&self, other: &Self, t: f32) -> Self { + self.lerp(*other, t) + } +} +``` +Other forms of interpolation are possible; for example, given two points and tangent vectors at each of +them, one might perform Hermite interpolation to create a cubic curve between them, whose points and tangent +values can then be sampled at any point on the curve. Another common example would be "spherical linear +interpolation" (`slerp`) of quaternions, used in animation and other rigid motions. + +To aid you in using this trait, `Interpolable` has blanket implementations for tuples whose members are +`Interpolable` (which simultaneously interpolate each constituent); in the same way, the `Interpolable` trait +can be derived for structs whose members are `Interpolable` with `#[derive(Interpolable)]`: +```rust +impl Interpolable for (S, T) +where + S: Interpolable, + T: Interpolable, +{ //... } +//... And so on for (S, T, U), etc. +``` +```rust +#[derive(Interpolable)] +struct MyCurveData { + position: Vec3, + velocity: Vec3, +} +``` + +### The Curve API + +Now, let us turn our attention back to `Curve`, which exposes a functional API similar to that of `Iterator`. +We will explore its main components one-by-one. The first of those is `map`: +```rust +/// Create a new curve by mapping the values of this curve via a function `f`; i.e., if the +/// sample at time `t` for this curve is `x`, the value at time `t` on the new curve will be +/// `f(x)`. +fn map(self, f: impl Fn(T) -> S) -> impl Curve +where + Self: Sized, + S: Interpolable, +{ //... } +``` +As you can see, `map` takes our curve and, consuming it, produces a new curve whose sample values are the images +under `f` of those from the function that we started with. For example, if we started with a curve in +three-dimensional space and wanted to project it onto the XY-plane, we could do that with `map`: +```rust +// A 3d curve, implementing `Curve` +let my_3d_curve = function_curve(2.0, |t| Vec3::new(t * t, 2.0 * t, t - 1.0)); +// Its 2d projection, implementing `Curve` +let my_2d_curve = my_3d_curve.map(|v| Vec2::new(vec.x, vec.y)); +``` +As you might expect, `map` is lazy like its `Iterator` counterpart, so the function it takes as input is only +evaluated when the resulting curve is actually sampled. + +--- + +Next up is `reparametrize`, one of the most important API methods: +```rust +/// Create a new [`Curve`] whose parameter space is related to the parameter space of this curve +/// by `f`. For each time `t`, the sample from the new curve at time `t` is the sample from +/// this curve at time `f(t)`. The given `duration` will be the duration of the new curve. The +/// function `f` is expected to take `[0, duration]` into `[0, self.duration]`. +fn reparametrize(self, duration: f32, f: impl Fn(f32) -> f32) -> impl Curve +where + Self: Sized, +{ //... } +``` +As you can see, `reparametrize` is like `map`, but the function is applied in parameter space instead of in +output space. This is somewhat counterintuitive, because it means that many of the functions that we might want +to use it with actually need to be inverted. For example, here is how you would use `reparametrize` to change +the `duration` of a curve from `1.0` to `2.0` by linearly stretching it: +```rust +let my_curve = function_curve(1.0, |x| x + 1.0); +let dur = my_curve.duration(); +let scaled_curve = my_curve.reparametrize(dur * 2.0, |t| t / 2.0); +``` +(A convenience method `reparametrize_linear` exists for this specific thing.) + +However, `reparametrize` is vastly more powerful than just this. For example, here we use `reparametrize` to +create a new curve that is a segment of the original one: +```rust +let my_curve = function_curve(1.0, |x| x * 2.0); +// The segment of `my_curve` from `0.5` to `1.0`: +let curve_segment = my_curve.reparametrize(0.5, |t| 0.5 + t); +``` + +And here, we use it to reverse our curve: +```rust +let my_curve = function_curve(2.0, |x| x * x); +let dur = my_curve.duration(); +let reversed_curve = my_curve.reparametrize(dur, |t| dur - t); +``` + +And here, we reparametrize by an easing curve: +```rust +let my_curve = function_curve(1.0, |x| x + 5.0); +let easing_curve = function_curve(1.0, |x| x * x * x); +let eased_curve = my_curve.reparametrize(1.0, |t| easing_curve.sample(t)); +``` +(The latter also has a convenience method, `reparametrize_by_curve`, which handles the duration automatically.) + +--- + +Next, we have `graph`, the last of the general functional methods of the API: +```rust +/// Create a new [`Curve`] which is the graph of this one; that is, its output includes the +/// parameter itself in the samples. For example, if this curve outputs `x` at time `t`, then +/// the produced curve will produce `(t, x)` at time `t`. +fn graph(self) -> impl Curve<(f32, T)> +where + Self: Sized, +{ //... } +``` +This is a subtle method whose main applications involve allowing more complex things to be expressed. For example, +here we modify a curve by making its output value attenuate with time: +```rust +// A curve with a value of `3.0` over a duration of `5.0`: +let my_curve = const_curve(5.0, 3.0); +// The same curve but with exponential falloff in time: +let new_curve = my_curve.graph().map(|(t, x)| x * (-t).exp2()); +``` -- Introducing new named concepts. -- Explaining the feature, ideally through simple examples of solutions to concrete problems. -- Explaining how Bevy users should *think* about the feature, and how it should impact the way they use Bevy. It should explain the impact as concretely as possible. -- If applicable, provide sample error messages, deprecation warnings, or migration guidance. -- If applicable, explain how this feature compares to similar existing features, and in what situations the user would use each one. +Most general operations that one could think up for curves can be achieved by clever combinations of `map` and +`reparametrize`, perhaps with a little `graph` sprinkled in. + +--- + +The next two API methods concern themselves with more imperative (i.e. data-focused) matters. Often one needs to +take a curve and actually render it concrete by sampling it at some resolution — for the purposes of storage, +serialization, application of numerical methods, and so on. That's where `resample` comes in: +```rust +/// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally +/// spaced values. A total of `samples` samples are used. +fn resample(&self, samples: usize) -> SampleCurve { //... } +``` +The main thing to notice about this is that, unlike the previous functional methods which merely spit out some kind +of `impl Curve`, `resample` has a concrete return type of `SampleCurve`, which is an actual struct that holds +actual data that you can access. + +Notably, `SampleCurve` still implements `Curve`, but its curve implementation is set in stone: it uses +equally spaced sample points together with the interpolation provided by `T` to provide something which is loosely +equivalent to your original curve, perhaps with a loss of fine detail. The sampling resolution is controlled by +the `samples` parameter, so higher values will ensure something that closely matches your curve. + +For example, with a `Curve` whose `T` performs linear interpolation, a `samples` value of 2 will yield a +`SampleCurve` that represents a line passing between the two endpoints, and as `samples` grows larger, +the original curve is approximated by greater and greater quantities of line segments equally spaced in its parameter +domain. + +Commonly, `resample` is used to obtain something more concrete after applying the `map` and `reparametrize` methods: +```rust +let my_curve = function_curve(3.0, |x| x * 2.0 + 1.0); +let modified_curve = my_curve.reparametrize(1.0, |t| 3.0 * t); +for (t, v) in modified_curve.graph().resample(100).samples { + println!("Value of {v} at time {t}"); +} +``` + +A variant of `resample` called `resample_uneven` allows for choosing the sample points directly instead of having them +evenly spaced: +```rust +/// Resample this [`Curve`] to produce a new one that is defined by interpolation over samples +/// taken at the given set of times. The given `sample_times` are expected to be strictly +/// increasing and nonempty. +fn resample_uneven(&self, sample_times: impl IntoIterator) -> UnevenSampleCurve { //... } +``` +This is useful for strategically saving space when performing serialization and storage. + +### Making curves + +The curve-creation functions `constant_curve` and `function_curve` that we have been using in examples are, in fact, +real functions that are part of the library. They look like this: +```rust +/// Create a [`Curve`] that constantly takes the given `value` over the given `duration`. +pub fn constant_curve(duration: f32, value: T) -> impl Curve { //... } +``` +```rust +/// Convert the given function `f` into a [`Curve`] with the given `duration`, sampled by +/// evaluating the function. +pub fn function_curve(duration: f32, f: F) -> impl Curve +where + T: Interpolable, + F: Fn(f32) -> T, +{ //... } +``` +Note that, while the examples used only functions `f32 -> f32`, `function_curve` can convert any function of the +parameter domain (valued in something `Interpolable`) into a `Curve`. For example, here is a rotation over time, +expressed as a `Curve` using this API: +```rust +let rotation_curve = function_curve(f32::consts::TAU, |t| Quat::from_rotation_z(t)); +``` +Furthermore, all of `bevy_math`'s curves (e.g. those created by splines) implement `Curve` for suitable values of +`T`, including information like derivatives in addition to positional data. Additionally, authors of other Bevy +libraries and internal modules may provide additional `Curve` implementors, either to provide functionality +for specific problem domains or to expand the variety of curve constructions available in the Bevy ecosystem. + +It is worth remembering that implementing `Curve` yourself, too, is extraordinarily straightforward, since the +only required methods are `duration` and `sample`, so you can hook into this API functionality yourself with ease. ## Implementation strategy -This is the technical portion of the RFC. -Try to capture the broad implementation strategy, -and then focus in on the tricky details so that: +The API is really segregated into two parts, the functional and the concrete. The functional part, whose outputs are +only guaranteed to be `impl Curve` of some kind, uses wrapper structs for its outputs, which take ownership of +the original curve data, along with any closures needed to perform combined sampling. For example, `map` is powered +by `MapCurve`, which looks like this: +```rust +/// A [`Curve`] whose samples are defined by mapping samples from another curve through a +/// given function. +pub struct MapCurve +where + S: Interpolable, + T: Interpolable, + C: Curve, + F: Fn(S) -> T, +{ + preimage: C, + f: F, + _phantom: PhantomData<(S, T)>, +} + +impl Curve for MapCurve +where + S: Interpolable, + T: Interpolable, + C: Curve, + F: Fn(S) -> T, +{ + fn duration(&self) -> f32 { + self.preimage.duration() + } + fn sample(&self, t: f32) -> T { + (self.f)(self.preimage.sample(t)) + } +} +``` +There are two things to be noted here: +- Since it takes ownership of a closure along with the source curve, which has unspecified serialization properties, +a `MapCurve` cannot in general be serialized or used in storage. +- This is just the default implementation, which can be overridden by other constructions where it makes sense. -- Its interaction with other features is clear. -- It is reasonably clear how the feature would be implemented. -- Corner cases are dissected by example. +The implementation of `reparametrize` is similar, relying on a `ReparamCurve` which owns the source curve +in addition to the reparametrization function. The function `graph` is powered by `GraphCurve` which, like the others, +must also own its source data; however, it doesn't require any function information, so it is essentially a plain +wrapper struct on the level of data (providing only a different implementation of `Curve::sample`). -When necessary, this section should return to the examples given in the previous section and explain the implementation details that make them work. +On the other hand, the "concrete" part of the API consists of `SampleCurve`, `UnevenSampleCurve`, and the +methods that yield them. These are implemented essentially as one would imagine, holding vectors of data and interpolating +it in `Curve::sample`. E.g.: -When writing this section be mindful of the following [repo guidelines](https://github.com/bevyengine/rfcs): +```rust +/// A [`Curve`] that is defined by neighbor interpolation over a set of samples. +pub struct SampleCurve +where + T: Interpolable, +{ + duration: f32, -- **RFCs should be scoped:** Try to avoid creating RFCs for huge design spaces that span many features. Try to pick a specific feature slice and describe it in as much detail as possible. Feel free to create multiple RFCs if you need multiple features. -- **RFCs should avoid ambiguity:** Two developers implementing the same RFC should come up with nearly identical implementations. -- **RFCs should be "implementable":** Merged RFCs should only depend on features from other merged RFCs and existing Bevy features. It is ok to create multiple dependent RFCs, but they should either be merged at the same time or have a clear merge order that ensures the "implementable" rule is respected. + /// The list of samples that define this curve by interpolation. + pub samples: Vec, +} +// ... + + #[inline] + fn sample(&self, t: f32) -> T { + let num_samples = self.samples.len(); + // If there is only one sample, then we return the single sample point. We also clamp `t` + // to `[0, self.duration]` here. + if num_samples == 1 || t <= 0.0 { + return self.samples[0].clone(); + } + if t >= self.duration { + return self.samples[self.samples.len() - 1].clone(); + } + + // Inside the curve itself, interpolate between the two nearest sample values. + let subdivs = num_samples - 1; + let step = self.duration / subdivs as f32; + let lower_index = (t / step).floor() as usize; + let upper_index = (t / step).ceil() as usize; + let f = (t / step).fract(); + self.samples[lower_index].interpolate(&self.samples[upper_index], f) + } +``` + +The main thing here is that `SampleCurve` is actually returned *by type* from the `resample` API, +and it is clearly suitable for serialization and storage in addition to numerical applications. This +allows consumers to work flexibly with the functional API and then cast down to concrete values when +they need them. (And of course, `UnevenSampleCurve` is implemented similarly, instead using a binary +search in its sequence of time-values to find its interpolation interval.) + +Because these types actually store concrete sample data, they have special implementations of `map` +which are not lazy, instead returning values of type `SampleCurve`/`UnevenSampleCurve`. These +can be accessed from a type-level API as `map_concrete`, so that the user can avoid erasing the +type if it is convenient to do so. The same goes for `graph`; however, because of its contravariance, +the same cannot be said of `reparametrize`, which maintains its default functional implementation. + +When `resample` and `resample_uneven` are told to sample 0 or 1 points, they should panic or return an error. +Empty curves are not considered conceptually valid. + +Everything else should be fairly clear based on the user-facing API descriptions. ## Drawbacks -Why should we *not* do this? +The main risk in implementing this is that we cement a poor or incomplete curve API within the Bevy +ecosystem, so that authors of internal and external modules need to sidestep it or do reimplementation +themselves. To avoid this, we should ensure that this system is adequately all-encompassing if we +choose to adopt it. ## Rationale and alternatives -- Why is this design the best in the space of possible designs? -- What other designs have been considered and what is the rationale for not choosing them? -- What objections immediately spring to mind? How have you addressed them? -- What is the impact of not doing this? -- Why is this important to implement as a feature of Bevy itself, rather than an ecosystem crate? +An API like `Curve` is natural for a problem domain in which underlying data is extremely varied; +the only real alternatives involve cementing ourselves around limited data-implementations that are +necessarily inadequate or inefficient for some problem domains. By contrast, the `Curve` API is +extremely flexible, allowing concrete data to interoperate with function closures before being +resampled again, serialized, and so on. These benefits apply immediately to any curve implementation +that can satisfy the measly requirements of `Curve`. -## \[Optional\] Prior art +One of the main apparent drawbacks of this interface is that the functional API methods `map`, +`reparametrize`, and `graph` implicitly perform type erasure, and this has implications for +serialization. For instance, even if an implementor `MyCurveType: Curve` also holds serialization +and deserialization traits (or other traits of relevance), and *even if these traits are implemented +by the output type of its mapping methods*, they cannot be accessed in the output value. I believe +that, if the need arises, this can be addressed by developing a small extension (say, `ConcreteCurve`) +to `Curve`, with methods like `map_concrete` whose output data are known to have stronger +guarantees. With just the present API alone, this would have to be addressed by resampling. -Discuss prior art, both the good and the bad, in relation to this proposal. -This can include: +This cannot merely be an ecosystem crate because the purpose is for a part of the ecosystem to +centralize around it. -- Does this feature exist in other libraries and what experiences have their community had? -- Papers: Are there any published papers or great posts that discuss this? +Finally, if we do not implement something like this, we lose out on the opportunity to centralize +a fragemented area. We also lose out on the opportunity to provide exceptionally useful functionality +to a great many users and inadvertently stifling future innovation in related areas, since implementation +burden becomes so much greater. -This section is intended to encourage you as an author to think about the lessons from other tools and provide readers of your RFC with a fuller picture. +## Unresolved questions -Note that while precedent set by other engines is some motivation, it does not on its own motivate an RFC. +Do we need more than this at the level of `bevy_math` to meet the needs of `bevy_animation` and perhaps `bevy_audio`? -## Unresolved questions +Are there other major stakeholders that I haven't thought of? + +I would also like additional thoughts related to the matter of serialization/deserialization that I raised +in the preceding section. + +## Future possibilities -- What parts of the design do you expect to resolve through the RFC process before this gets merged? -- What parts of the design do you expect to resolve through the implementation of this feature before the feature PR is merged? -- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? +This paves the groundwork for future work on curve geometry. Among the lowest-hanging fruit after the +fact would be an algorithmic implementation of the Implicit Function Theorem, which also swiftly leads +to arclength reparametrization of any curve with enough data (e.g. positions and derivatives), a feature +that has been requested in the context of cubic curves already. -## \[Optional\] Future possibilities +In particular, I imagine a library built on top of this which takes specifically geometric curve data +and operates algorithmically on it to produce things like: +- derivative estimation from positions (easy but non-obvious) +- arclength estimation and reparametrization (easy with IFT) +- rotation-minimizing frames for arbitrary regular curves (easy) -Think about what the natural extension and evolution of your proposal would -be and how it would affect Bevy as a whole in a holistic way. -Try to use this section as a tool to more fully consider other possible -interactions with the engine in your proposal. +Further along that path are things like mesh extrusion. In particular, I really view this as the first step +in the program of making roads in Bevy. -This is also a good place to "dump ideas", if they are out of scope for the -RFC you are writing but otherwise related. +It's possible also that the API itself might like to be expanded — by introducing further convenience methods, +more sophistocated forms of resampling, and so on. Some of these will, no doubt, rely on additional +specialization of the interface to more particular values of `T` that are more closely associated to +particular problem domains. -Note that having something written down in the future-possibilities section -is not a reason to accept the current or a future RFC; such notes should be -in the section on motivation or rationale in this or subsequent RFCs. -If a feature or change has no direct value on its own, expand your RFC to include the first valuable feature that would build on it. From 8d4c758b2eb27199629a30799ecd32ac1b038a0a Mon Sep 17 00:00:00 2001 From: Matty Date: Thu, 11 Apr 2024 19:42:02 -0400 Subject: [PATCH 03/22] Rename curve-trait.md to 80-curve-trait.md --- rfcs/{curve-trait.md => 80-curve-trait.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rfcs/{curve-trait.md => 80-curve-trait.md} (100%) diff --git a/rfcs/curve-trait.md b/rfcs/80-curve-trait.md similarity index 100% rename from rfcs/curve-trait.md rename to rfcs/80-curve-trait.md From 706b1a4b9c17c989b5ae24b959e61dc80255d28c Mon Sep 17 00:00:00 2001 From: Matty Date: Sat, 13 Apr 2024 15:38:02 -0400 Subject: [PATCH 04/22] Added `Interval`, `zip`, `sample_checked`, `sample_clamped` --- rfcs/80-curve-trait.md | 228 +++++++++++++++++++++++++++++------------ 1 file changed, 162 insertions(+), 66 deletions(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index 00eb0727..1cc53627 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -36,17 +36,17 @@ pub trait Curve where T: Interpolable, { - fn duration(&self) -> f32; + fn domain(&self) -> Interval; fn sample(&self, t: f32) -> T; } ``` -At a basic level, it encompasses values of type `T` parametrized over an interval that starts at 0 and ends -at the curve's given `duration`, allowing that data to be sampled by providing the associated time parameter. +At a basic level, it encompasses values of type `T` parametrized over some particular `Interval`, allowing +that data to be sampled by providing the associated time parameter. ### Interpolation -The only requirement of `T` to be used with `Curve` is that it implements the `Interpolable` trait, so we -will briefly examine that next: +For `T` to be used with `Curve`, it must implement the `Interpolable` trait, so we will briefly examine that +next: ```rust pub trait Interpolable: Clone { @@ -98,7 +98,63 @@ struct MyCurveData { } ``` -### The Curve API +### Intervals + +The other auxiliary component at play in the definition of `Curve` is `Interval`. This is a type which +represents a nonempty closed interval which may be infinite in either direction. Its provided methods are +mostly self-explanatory: +```rust +/// Create a new [`Interval`] with the specified `start` and `end`. The interval can be infinite +/// but cannot be empty; invalid parameters will result in an error. +pub fn new(start: f32, end: f32) -> Result { //... } + +/// Get the start of this interval. +pub fn start(self) -> f32 { //... } + +/// Get the end of this interval. +pub fn end(self) -> f32 { //... } + +/// Get the length of this interval. Note that the result may be infinite (`f32::INFINITY`). +pub fn length(self) -> f32 { //... } + +/// Returns `true` if this interval is finite. +pub fn is_finite(self) -> bool { //... } + +/// Returns `true` if `item` is contained in this interval. +pub fn contains(self, item: f32) -> bool { //... } + +/// Create an [`Interval`] by intersecting this interval with another. Returns an error if the +/// intersection would be empty (hence an invalid interval). +pub fn intersect(self, other: Interval) -> Result { //... } + +/// Clamp the given `value` to lie within this interval. +pub fn clamp(self, value: f32) -> f32 { //... } + +/// Get the linear map which maps this curve onto the `other` one. Returns an error if either +/// interval is infinite. +pub fn linear_map_to(self, other: Self) -> Result f32, InfiniteIntervalError> { //... } +``` + +The `Interval` type also implements `TryFrom`, which may be desirable if you want to use +the `start..=end` syntax. One of the primary benefits of `Interval` (in addition to these methods) is +that it is `Copy`, so it is easy to take intervals and throw them around. + +### Sampling + +The `Curve::sample` method is not intrinsically constrained by the curve's `domain` interval. Instead, +implementors of `Curve` are free to determine how samples drawn from outside the `domain` will behave. +However, variants of `sample` (as well as other important methods) use the `domain` explicitly: +```rust +/// Sample a point on this curve at the parameter value `t`, returning `None` if the point is +/// outside of the curve's domain. +fn sample_checked(&self, t: f32) -> Option { //... } + +/// Sample a point on this curve at the parameter value `t`, clamping `t` to lie inside the +/// domain of the curve. +fn sample_clamped(&self, t: f32) -> T { //... } +``` + +### The Main Curve API Now, let us turn our attention back to `Curve`, which exposes a functional API similar to that of `Iterator`. We will explore its main components one-by-one. The first of those is `map`: @@ -130,9 +186,9 @@ Next up is `reparametrize`, one of the most important API methods: ```rust /// Create a new [`Curve`] whose parameter space is related to the parameter space of this curve /// by `f`. For each time `t`, the sample from the new curve at time `t` is the sample from -/// this curve at time `f(t)`. The given `duration` will be the duration of the new curve. The -/// function `f` is expected to take `[0, duration]` into `[0, self.duration]`. -fn reparametrize(self, duration: f32, f: impl Fn(f32) -> f32) -> impl Curve +/// this curve at time `f(t)`. The given `domain` will be the domain of the new curve. The +/// function `f` is expected to take `domain` into `self.domain()`. +fn reparametrize(self, domain: Interval, f: impl Fn(f32) -> f32) -> impl Curve where Self: Sized, { //... } @@ -140,36 +196,36 @@ where As you can see, `reparametrize` is like `map`, but the function is applied in parameter space instead of in output space. This is somewhat counterintuitive, because it means that many of the functions that we might want to use it with actually need to be inverted. For example, here is how you would use `reparametrize` to change -the `duration` of a curve from `1.0` to `2.0` by linearly stretching it: +the `domain` of a curve from `[0.0, 1.0]` to `[0.0, 2.0]` by linearly stretching it: ```rust -let my_curve = function_curve(1.0, |x| x + 1.0); -let dur = my_curve.duration(); -let scaled_curve = my_curve.reparametrize(dur * 2.0, |t| t / 2.0); +let my_curve = function_curve(interval(0.0, 1.0).unwrap(), |x| x + 1.0); +let domain = my_curve.domain(); +let scaled_curve = my_curve.reparametrize(interval(0.0, 2.0).unwrap(), |t| t / 2.0); ``` (A convenience method `reparametrize_linear` exists for this specific thing.) However, `reparametrize` is vastly more powerful than just this. For example, here we use `reparametrize` to create a new curve that is a segment of the original one: ```rust -let my_curve = function_curve(1.0, |x| x * 2.0); -// The segment of `my_curve` from `0.5` to `1.0`: -let curve_segment = my_curve.reparametrize(0.5, |t| 0.5 + t); +let my_curve = function_curve(interval(0.0, 1.0).unwrap(), |x| x * 2.0); +// The segment of `my_curve` from `0.5` to `1.0`, shifted back to `0.0: +let curve_segment = my_curve.reparametrize(interval(0.0, 0.5).unwrap(), |t| 0.5 + t); ``` And here, we use it to reverse our curve: ```rust -let my_curve = function_curve(2.0, |x| x * x); -let dur = my_curve.duration(); -let reversed_curve = my_curve.reparametrize(dur, |t| dur - t); +let my_curve = function_curve(interval(0.0, 2.0).unwrap(), |x| x * x); +let domain = my_curve.domain(); +let reversed_curve = my_curve.reparametrize(domain, |t| domain.end() - t); ``` And here, we reparametrize by an easing curve: ```rust -let my_curve = function_curve(1.0, |x| x + 5.0); -let easing_curve = function_curve(1.0, |x| x * x * x); -let eased_curve = my_curve.reparametrize(1.0, |t| easing_curve.sample(t)); +let my_curve = function_curve(interval(0.0, 1.0).unwrap(), |x| x + 5.0); +let easing_curve = function_curve(interval(0.0, 1.0).unwrap(), |x| x * x * x); +let eased_curve = my_curve.reparametrize(easing_curve.domain(), |t| easing_curve.sample(t)); ``` -(The latter also has a convenience method, `reparametrize_by_curve`, which handles the duration automatically.) +(The latter also has a convenience method, `reparametrize_by_curve`, which handles the domain automatically.) --- @@ -186,8 +242,8 @@ where This is a subtle method whose main applications involve allowing more complex things to be expressed. For example, here we modify a curve by making its output value attenuate with time: ```rust -// A curve with a value of `3.0` over a duration of `5.0`: -let my_curve = const_curve(5.0, 3.0); +// A curve with a value of `3.0` over the `Interval` from `0.0` to `5.0`: +let my_curve = const_curve(interval(0.0, 5.0).unwrap(), 3.0); // The same curve but with exponential falloff in time: let new_curve = my_curve.graph().map(|(t, x)| x * (-t).exp2()); ``` @@ -197,13 +253,27 @@ Most general operations that one could think up for curves can be achieved by cl --- +Another useful API method is `zip`, which behaves much like its `Iterator` counterpart: +```rust +/// Create a new [`Curve`] by joining this curve together with another. The sample at time `t` +/// in the new curve is `(x, y)`, where `x` is the sample of `self` at time `t` and `y` is the +/// sample of `other` at time `t`. The domain of the new curve is the intersection of the +/// domains of its constituents. If the domain intersection would be empty, an +/// [`InvalidIntervalError`] is returned. +fn zip(self, other: C) -> Result, InvalidIntervalError> { //... } +``` + +--- + The next two API methods concern themselves with more imperative (i.e. data-focused) matters. Often one needs to take a curve and actually render it concrete by sampling it at some resolution — for the purposes of storage, serialization, application of numerical methods, and so on. That's where `resample` comes in: ```rust /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally -/// spaced values. A total of `samples` samples are used. -fn resample(&self, samples: usize) -> SampleCurve { //... } +/// spaced values. A total of `samples` samples are used, although at least two samples are +/// required in order to produce well-formed output. If fewer than two samples are provided, +/// or if this curve has an unbounded domain, then a [`ResamplingError`] is returned. +fn resample(&self, samples: usize) -> Result, ResamplingError> { //... } ``` The main thing to notice about this is that, unlike the previous functional methods which merely spit out some kind of `impl Curve`, `resample` has a concrete return type of `SampleCurve`, which is an actual struct that holds @@ -221,9 +291,9 @@ domain. Commonly, `resample` is used to obtain something more concrete after applying the `map` and `reparametrize` methods: ```rust -let my_curve = function_curve(3.0, |x| x * 2.0 + 1.0); -let modified_curve = my_curve.reparametrize(1.0, |t| 3.0 * t); -for (t, v) in modified_curve.graph().resample(100).samples { +let my_curve = function_curve(interval(0.0, 3.0).unwrap(), |x| x * 2.0 + 1.0); +let modified_curve = my_curve.reparametrize(interval(0.0, 1.0).unwrap(), |t| 3.0 * t).unwrap(); +for (t, v) in modified_curve.graph().resample(100).sample_iter() { println!("Value of {v} at time {t}"); } ``` @@ -232,24 +302,37 @@ A variant of `resample` called `resample_uneven` allows for choosing the sample evenly spaced: ```rust /// Resample this [`Curve`] to produce a new one that is defined by interpolation over samples -/// taken at the given set of times. The given `sample_times` are expected to be strictly -/// increasing and nonempty. -fn resample_uneven(&self, sample_times: impl IntoIterator) -> UnevenSampleCurve { //... } +/// taken at the given set of times. The given `sample_times` are expected to contain at least +/// two valid times within the curve's domain range. +/// +/// Irredundant sample times, non-finite sample times, and sample times outside of the domain +/// are simply filtered out. With an insufficient quantity of data, a [`ResamplingError`] is +/// returned. +/// +/// The domain of the produced [`UnevenSampleCurve`] stretches between the first and last +/// sample times of the iterator. +fn resample_uneven( + &self, + sample_times: impl IntoIterator, +) -> Result, ResamplingError> { //... } ``` -This is useful for strategically saving space when performing serialization and storage. +This is useful for strategically saving space when performing serialization and storage; uneven samples are something +like animation keyframes. The `UnevenSampleCurve` type also supports *forward* mapping of its sample times via +the method `map_sample_times`, which accepts a function like the inverse of the one that would be used in +`Curve::reparametrize`. ### Making curves The curve-creation functions `constant_curve` and `function_curve` that we have been using in examples are, in fact, real functions that are part of the library. They look like this: ```rust -/// Create a [`Curve`] that constantly takes the given `value` over the given `duration`. -pub fn constant_curve(duration: f32, value: T) -> impl Curve { //... } +/// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. +pub fn constant_curve(domain: Interval, value: T) -> impl Curve { //... } ``` ```rust -/// Convert the given function `f` into a [`Curve`] with the given `duration`, sampled by +/// Convert the given function `f` into a [`Curve`] with the given `domain`, sampled by /// evaluating the function. -pub fn function_curve(duration: f32, f: F) -> impl Curve +pub fn function_curve(domain: Interval, f: F) -> impl Curve where T: Interpolable, F: Fn(f32) -> T, @@ -259,7 +342,7 @@ Note that, while the examples used only functions `f32 -> f32`, `function_curve` parameter domain (valued in something `Interpolable`) into a `Curve`. For example, here is a rotation over time, expressed as a `Curve` using this API: ```rust -let rotation_curve = function_curve(f32::consts::TAU, |t| Quat::from_rotation_z(t)); +let rotation_curve = function_curve(interval(0.0, f32::consts::TAU).unwrap(), |t| Quat::from_rotation_z(t)); ``` Furthermore, all of `bevy_math`'s curves (e.g. those created by splines) implement `Curve` for suitable values of `T`, including information like derivatives in addition to positional data. Additionally, authors of other Bevy @@ -267,7 +350,7 @@ libraries and internal modules may provide additional `Curve` implementors, e for specific problem domains or to expand the variety of curve constructions available in the Bevy ecosystem. It is worth remembering that implementing `Curve` yourself, too, is extraordinarily straightforward, since the -only required methods are `duration` and `sample`, so you can hook into this API functionality yourself with ease. +only required methods are `domain` and `sample`, so you can hook into this API functionality yourself with ease. ## Implementation strategy @@ -297,8 +380,8 @@ where C: Curve, F: Fn(S) -> T, { - fn duration(&self) -> f32 { - self.preimage.duration() + fn domain(&self) -> Interval { + self.preimage.domain() } fn sample(&self, t: f32) -> T { (self.f)(self.preimage.sample(t)) @@ -315,6 +398,30 @@ in addition to the reparametrization function. The function `graph` is powered b must also own its source data; however, it doesn't require any function information, so it is essentially a plain wrapper struct on the level of data (providing only a different implementation of `Curve::sample`). +A minor point is that `MapCurve` and `ReparamCurve` support special implementations of `map` and `reparametrize`, +which allow data reuse instead of repeatedly wrapping data when repeated calls to these functional APIs are made. +For example, the `map` implementation of `MapCurve` looks like this: +```rust +fn map(self, g: impl Fn(T) -> R) -> impl Curve +where + Self: Sized, + R: Interpolable, +{ + let gf = move |x| g((self.f)(x)); + MapCurve { + preimage: self.preimage, + f: gf, + _phantom: PhantomData, + } +} +``` + +Furthermore, when `reparametrize` and `map` are intermixed, they produce another data structure, `MapReparamCurve`, +which is capable of absorbing all future calls to `map` and `reparametrize` in a way essentially identical to that +demonstrated by the preceding code. + +--- + On the other hand, the "concrete" part of the API consists of `SampleCurve`, `UnevenSampleCurve`, and the methods that yield them. These are implemented essentially as one would imagine, holding vectors of data and interpolating it in `Curve::sample`. E.g.: @@ -325,31 +432,22 @@ pub struct SampleCurve where T: Interpolable, { - duration: f32, - - /// The list of samples that define this curve by interpolation. - pub samples: Vec, + domain: Interval, + samples: Vec, } // ... - #[inline] fn sample(&self, t: f32) -> T { - let num_samples = self.samples.len(); - // If there is only one sample, then we return the single sample point. We also clamp `t` - // to `[0, self.duration]` here. - if num_samples == 1 || t <= 0.0 { - return self.samples[0].clone(); - } - if t >= self.duration { - return self.samples[self.samples.len() - 1].clone(); - } + // We clamp `t` to the domain. + let t = self.domain.clamp(t); // Inside the curve itself, interpolate between the two nearest sample values. - let subdivs = num_samples - 1; - let step = self.duration / subdivs as f32; - let lower_index = (t / step).floor() as usize; - let upper_index = (t / step).ceil() as usize; - let f = (t / step).fract(); + let subdivs = self.samples.len() - 1; + let step = self.domain.length() / subdivs as f32; + let t_shifted = t - self.domain.start(); + let lower_index = (t_shifted / step).floor() as usize; + let upper_index = (t_shifted / step).ceil() as usize; + let f = (t_shifted / step).fract(); self.samples[lower_index].interpolate(&self.samples[upper_index], f) } ``` @@ -363,11 +461,9 @@ search in its sequence of time-values to find its interpolation interval.) Because these types actually store concrete sample data, they have special implementations of `map` which are not lazy, instead returning values of type `SampleCurve`/`UnevenSampleCurve`. These can be accessed from a type-level API as `map_concrete`, so that the user can avoid erasing the -type if it is convenient to do so. The same goes for `graph`; however, because of its contravariance, -the same cannot be said of `reparametrize`, which maintains its default functional implementation. - -When `resample` and `resample_uneven` are told to sample 0 or 1 points, they should panic or return an error. -Empty curves are not considered conceptually valid. +type if it is convenient to do so. The same goes for `graph`; however, because of its contravariance +(it is function precomposition), the same cannot be said of `reparametrize`, which maintains its default +functional implementation. The latter gap is filled partially by `UnevenSampleCurve::map_sample_times`. Everything else should be fairly clear based on the user-facing API descriptions. From fd08807e3654fe1eade6563a0a2b607f7dc29b9e Mon Sep 17 00:00:00 2001 From: Matty Date: Tue, 16 Apr 2024 06:58:16 -0400 Subject: [PATCH 05/22] Added examples of curve applications in games --- rfcs/80-curve-trait.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index 1cc53627..cc6966ab 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -9,6 +9,13 @@ animation keyframes), among others. The purpose is to provide a common baseline of useful functionality that unites these disparate implementations, making curves of many kinds pleasant to work with for authors of plugins and games. +Common uses of curves in games include: +* Describing animations as transformations over time +* Describing movement of game entities +* Describing camera moves +* Describing easing and attenuation with time and/or distance +* Describing geometry (e.g. of roads, surfaces of extrusion, etc.) +* Describing paths of particles and effects ## Motivation From 43f2e338bbad07a94057471e39102514095610a2 Mon Sep 17 00:00:00 2001 From: Matty Date: Thu, 18 Apr 2024 11:20:00 -0400 Subject: [PATCH 06/22] Added commentary on borrowing and object safety --- rfcs/80-curve-trait.md | 43 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index cc6966ab..b1b57fae 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -359,8 +359,26 @@ for specific problem domains or to expand the variety of curve constructions ava It is worth remembering that implementing `Curve` yourself, too, is extraordinarily straightforward, since the only required methods are `domain` and `sample`, so you can hook into this API functionality yourself with ease. +### Borrowing + +One other minor point is that you may not always want functions like `map` and `reparametrize` to take ownership of +the input curve. For example, the following code takes ownership of `my_curve`, so it cannot be reused, even though +`resample` requires only a reference: +```rust +let mapped_sample_curve = my_curve.map(|x| x * 2.0).resample(100).unwrap(); +``` +In order to circumvent this, the `by_ref` method exists, supported by a blanket implementation which allows any +type which dereferences to a `Curve` to be a `Curve` itself. For example, the following are essentially equivalent +and do not take ownership of `my_curve`: +```rust +let mapped_sample_curve = my_curve.by_ref().map(|x| x * 2.0).resample(100).unwrap(); +let mapped_sample_curve = (&my_curve).map(|x| x * 2.0).resample(100).unwrap(); +``` + ## Implementation strategy +### API Implementation + The API is really segregated into two parts, the functional and the concrete. The functional part, whose outputs are only guaranteed to be `impl Curve` of some kind, uses wrapper structs for its outputs, which take ownership of the original curve data, along with any closures needed to perform combined sampling. For example, `map` is powered @@ -472,7 +490,30 @@ type if it is convenient to do so. The same goes for `graph`; however, because o (it is function precomposition), the same cannot be said of `reparametrize`, which maintains its default functional implementation. The latter gap is filled partially by `UnevenSampleCurve::map_sample_times`. -Everything else should be fairly clear based on the user-facing API descriptions. +Everything else should be fairly clear based on the user-facing API descriptions. + +### Object safety + +The `Curve` trait should be object-safe. While the functional `Curve` API methods are automatically +excluded from dynamic dispatch because they move `self` into another struct (and hence require `Self: Sized`), +others should be explicitly given the `Self: Sized` constraint in order to ensure object safety. + +This prevents methods like `map` from being used by `dyn Curve` (which was hopeless regardless); however, +by providing a blanket implementation over `Deref`, pointers to trait objects can still be used as `Curve`. +For instance, `Box>`, `Arc>`, etc. all implement `Curve` through this blanket +implementation, which means that they can use the default implementations of `map`, `reparametrize`, etc. built +on top of the object-safe core containing `domain` and `sample`. + +This is the reason for including a `?Sized` constraint on the underlying curve in the blanket implementation, +which has a signature that looks like this: +```rust +impl Curve for D +where + T: Interpolable, + C: Curve + ?Sized, + D: Deref, +{ //... } +``` ## Drawbacks From 0109a28f91861970b438c459eebd4ccbb4862214 Mon Sep 17 00:00:00 2001 From: Matty Date: Fri, 19 Apr 2024 17:11:47 -0400 Subject: [PATCH 07/22] Apply some suggestions from code review Co-authored-by: Alice Cecile --- rfcs/80-curve-trait.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index b1b57fae..dd79c323 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -16,6 +16,7 @@ Common uses of curves in games include: * Describing easing and attenuation with time and/or distance * Describing geometry (e.g. of roads, surfaces of extrusion, etc.) * Describing paths of particles and effects +* Defining experience and drop rate curves ## Motivation @@ -86,7 +87,7 @@ them, one might perform Hermite interpolation to create a cubic curve between th values can then be sampled at any point on the curve. Another common example would be "spherical linear interpolation" (`slerp`) of quaternions, used in animation and other rigid motions. -To aid you in using this trait, `Interpolable` has blanket implementations for tuples whose members are +To make using `Interpolable` easier, it has a blanket implementation for tuples whose members are `Interpolable` (which simultaneously interpolate each constituent); in the same way, the `Interpolable` trait can be derived for structs whose members are `Interpolable` with `#[derive(Interpolable)]`: ```rust @@ -107,8 +108,8 @@ struct MyCurveData { ### Intervals -The other auxiliary component at play in the definition of `Curve` is `Interval`. This is a type which -represents a nonempty closed interval which may be infinite in either direction. Its provided methods are +The `Interval` associated type is the other part of the definition of each `Curve`. This is a type which +represents a nonempty closed interval over the `f32` numbers whose endpoints may be at infinity. Its provided methods are mostly self-explanatory: ```rust /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be infinite From f1b56d82274332d6c7d84a709de883bfeb71ced5 Mon Sep 17 00:00:00 2001 From: Matty Date: Sun, 21 Apr 2024 14:27:13 -0400 Subject: [PATCH 08/22] Snippet in drawbacks about maintenance burden etc. --- rfcs/80-curve-trait.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index dd79c323..902d08b9 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -520,8 +520,10 @@ where The main risk in implementing this is that we cement a poor or incomplete curve API within the Bevy ecosystem, so that authors of internal and external modules need to sidestep it or do reimplementation -themselves. To avoid this, we should ensure that this system is adequately all-encompassing if we -choose to adopt it. +themselves. + +Furthermore, a poor implementation would lead to additional maintenance burden and compile times with +little benefit. ## Rationale and alternatives From 8dcb1b0165e04d94fc33651d1b74c9983bbeae85 Mon Sep 17 00:00:00 2001 From: Matty Date: Sun, 21 Apr 2024 16:12:53 -0400 Subject: [PATCH 09/22] Partial rewrite of user-facing introduction --- rfcs/80-curve-trait.md | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index 902d08b9..af191963 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -38,7 +38,35 @@ be forced to repeat our work for each class of curves that we want to work with. ### Introduction -The trait `Curve` provides a generalized API surface for curves. Its requirements are quite simple: +The trait `Curve` provides a generalized API surface for curves. In principle, a `Curve` is a family +of values of type `T` parametrized over some interval which is typically thought of as something like time +or distance. + +For example, we might use the `Curve` APIs to construct a `Curve` in order to describe the motion +of an Entity with time: +```rust +// This is a `Curve` that describes a rotation with time: +let rotation_over_time = function_curve(interval(0.0, std::f32::consts::TAU).unwrap(), |t| Quat::from_rotation_z(t)); +// Here is a `Curve` that describes a translation with time: +let translation_over_time = function_curve(interval(0.0, 1.0).unwrap(), |t| Vec3::splat(t)); +// Let's reparametrize the rotation so that it goes from 0 to 1 instead: +let new_rotation = rotation_over_time.reparametrize_linear(interval(0.0, 1.0).unwrap()).unwrap(); +// Combine the two into a `Curve<(Vec3, Quat)>` by zipping them together: +let translation_and_rotation = translation_over_time.zip(new_rotation).unwrap(); +// Join the two families of data to get a `Curve`: +let transform_curve = translation_and_rotation.map(|(t, r)| Transform::from_translation(t).with_rotation(r)); +``` + +This could be used to actually set an entity's `Transform` in a system or otherwise: +```rust +*my_entity_transform = transform_curve.sample(0.6); +``` + +However, `Curve` is quite general, and it can be used for much more, including describing and manipulating +easings, animations, geometry, camera moves, and more! + +The trait itself looks like this: + ```rust pub trait Curve where @@ -48,8 +76,8 @@ where fn sample(&self, t: f32) -> T; } ``` -At a basic level, it encompasses values of type `T` parametrized over some particular `Interval`, allowing -that data to be sampled by providing the associated time parameter. +At a basic level, it encompasses values of type `T` parametrized over some particular `Interval` (its `domain`, +allowing that data to be sampled by providing the associated time parameter. ### Interpolation @@ -350,7 +378,7 @@ Note that, while the examples used only functions `f32 -> f32`, `function_curve` parameter domain (valued in something `Interpolable`) into a `Curve`. For example, here is a rotation over time, expressed as a `Curve` using this API: ```rust -let rotation_curve = function_curve(interval(0.0, f32::consts::TAU).unwrap(), |t| Quat::from_rotation_z(t)); +let rotation_curve = function_curve(interval(0.0, std::f32::consts::TAU).unwrap(), |t| Quat::from_rotation_z(t)); ``` Furthermore, all of `bevy_math`'s curves (e.g. those created by splines) implement `Curve` for suitable values of `T`, including information like derivatives in addition to positional data. Additionally, authors of other Bevy From c1908275cf02dd1812f648d68ccf2c20bf4371c9 Mon Sep 17 00:00:00 2001 From: Matty Date: Mon, 13 May 2024 14:53:38 -0400 Subject: [PATCH 10/22] Update rfcs/80-curve-trait.md Co-authored-by: Alice Cecile --- rfcs/80-curve-trait.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index af191963..048be787 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -29,7 +29,7 @@ disparate areas and, ideally, allow for authors to reuse the work of others rath wheel every time they want to write libraries that involve generating or working with curves. Furthermore, something like this is also a prerequisite to bringing more complex geometric operations -(some of them with broad implications) into `bevy_math` itself. For instance, this lays the natural bedwork +(some of them with broad implications) into `bevy_math` itself. For instance, this lays the natural foundation for more specialized libraries in curve geometry, such as those needed by curve extrusion (e.g. for mesh generation of roads and other surfaces). Without an internal curve abstraction in Bevy itself, we would be forced to repeat our work for each class of curves that we want to work with. From f8967c56d90f3046d22ccd34d03648880af0c4cc Mon Sep 17 00:00:00 2001 From: Matty Date: Mon, 13 May 2024 15:36:49 -0400 Subject: [PATCH 11/22] Updates from comments --- rfcs/80-curve-trait.md | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index 048be787..d9903ab4 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -204,9 +204,9 @@ where S: Interpolable, { //... } ``` -As you can see, `map` takes our curve and, consuming it, produces a new curve whose sample values are the images -under `f` of those from the function that we started with. For example, if we started with a curve in -three-dimensional space and wanted to project it onto the XY-plane, we could do that with `map`: +As you can see, `map` takes our curve and, consuming it, produces a new curve whose sample values are the sample +values of the starting curve mapped through `f`. For example, if we started with a curve in three-dimensional space +and wanted to project it onto the XY-plane, we could do that with `map`: ```rust // A 3d curve, implementing `Curve` let my_3d_curve = function_curve(2.0, |t| Vec3::new(t * t, 2.0 * t, t - 1.0)); @@ -396,9 +396,9 @@ the input curve. For example, the following code takes ownership of `my_curve`, ```rust let mapped_sample_curve = my_curve.map(|x| x * 2.0).resample(100).unwrap(); ``` -In order to circumvent this, the `by_ref` method exists, supported by a blanket implementation which allows any -type which dereferences to a `Curve` to be a `Curve` itself. For example, the following are essentially equivalent -and do not take ownership of `my_curve`: +The `by_ref` method exists to circumvent this problem, allowing curve data to be used through borrowing; this is +supported by a blanket implementation which allows any type which dereferences to a `Curve` to be a `Curve` itself. +For example, the following are equivalent and do not take ownership of `my_curve`: ```rust let mapped_sample_curve = my_curve.by_ref().map(|x| x * 2.0).resample(100).unwrap(); let mapped_sample_curve = (&my_curve).map(|x| x * 2.0).resample(100).unwrap(); @@ -553,6 +553,16 @@ themselves. Furthermore, a poor implementation would lead to additional maintenance burden and compile times with little benefit. +Another limitation of the API presented here is that it uses `f32` exclusively as its parameter type, +which presents another area where the API could be deemed incomplete in the future. The reason that `f32` +was deemed adequate are as follows: +- Existing constructions in `bevy_math`, `bevy_animation`, and `bevy_color` all use `f32`. +- Basically nothing on Bevy in the CPU side uses `f64` internally. +- Curve applications tend to be artistic in nature rather than demanding high precision, so that `f32` is + generally good enough. +- If strictly necessary, making the change to use generics is straightforward, but unifying around `f32` + is simpler and encourages low-effort interoperation (i.e. reusing data across domains is more likely). + ## Rationale and alternatives An API like `Curve` is natural for a problem domain in which underlying data is extremely varied; @@ -581,12 +591,7 @@ burden becomes so much greater. ## Unresolved questions -Do we need more than this at the level of `bevy_math` to meet the needs of `bevy_animation` and perhaps `bevy_audio`? - -Are there other major stakeholders that I haven't thought of? - -I would also like additional thoughts related to the matter of serialization/deserialization that I raised -in the preceding section. +I believe that all major questions for this particular project scope have been resolved satisfactorily. ## Future possibilities @@ -609,3 +614,6 @@ more sophistocated forms of resampling, and so on. Some of these will, no doubt, specialization of the interface to more particular values of `T` that are more closely associated to particular problem domains. +Another important future direction to keep in mind is the serialization and deserialization of curve data, +coupled with the use of items like `dyn Curve` and similar. This will likely require extensions of the +`Curve` API (or specializations thereof) to work well (e.g. a specialized subtrait). From b678b467895796814d067bedc60311aaed11675c Mon Sep 17 00:00:00 2001 From: Matty Date: Mon, 13 May 2024 15:37:52 -0400 Subject: [PATCH 12/22] Typo --- rfcs/80-curve-trait.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index d9903ab4..f722cb0d 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -557,7 +557,7 @@ Another limitation of the API presented here is that it uses `f32` exclusively a which presents another area where the API could be deemed incomplete in the future. The reason that `f32` was deemed adequate are as follows: - Existing constructions in `bevy_math`, `bevy_animation`, and `bevy_color` all use `f32`. -- Basically nothing on Bevy in the CPU side uses `f64` internally. +- Basically nothing in Bevy on the CPU side uses `f64` internally. - Curve applications tend to be artistic in nature rather than demanding high precision, so that `f32` is generally good enough. - If strictly necessary, making the change to use generics is straightforward, but unifying around `f32` From 0d741382fc061d0b42fa491b60667eb20e57e1f9 Mon Sep 17 00:00:00 2001 From: Matty Date: Thu, 6 Jun 2024 17:44:54 -0400 Subject: [PATCH 13/22] Rewrite to remove type-inferred interpolation from the core of the trait --- rfcs/80-curve-trait.md | 309 ++++++++++++++++++++++------------------- 1 file changed, 166 insertions(+), 143 deletions(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index f722cb0d..93713336 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -28,6 +28,9 @@ By providing a common baseline in `Curve`, we can ensure a significant degree disparate areas and, ideally, allow for authors to reuse the work of others rather than reinventing the wheel every time they want to write libraries that involve generating or working with curves. +Ideally, many APIs that consume curves will be able to use something like `impl Curve` (or even `dyn Curve` +where it makes sense) instead of relying on specific representations. + Furthermore, something like this is also a prerequisite to bringing more complex geometric operations (some of them with broad implications) into `bevy_math` itself. For instance, this lays the natural foundation for more specialized libraries in curve geometry, such as those needed by curve extrusion (e.g. for mesh @@ -69,8 +72,6 @@ The trait itself looks like this: ```rust pub trait Curve -where - T: Interpolable, { fn domain(&self) -> Interval; fn sample(&self, t: f32) -> T; @@ -79,64 +80,9 @@ where At a basic level, it encompasses values of type `T` parametrized over some particular `Interval` (its `domain`, allowing that data to be sampled by providing the associated time parameter. -### Interpolation - -For `T` to be used with `Curve`, it must implement the `Interpolable` trait, so we will briefly examine that -next: - -```rust -pub trait Interpolable: Clone { - fn interpolate(&self, other: &Self, t: f32) -> Self; -} -``` - -As you can see, in addition to the `Clone` requirement, `Interpolable` requires a single method, `interpolate`, -which takes two items by reference and produces a third by interpolating between them. The idea is that -when `t` is 0, `self` is recovered, whereas when `t` is 1, you receive `other`. Intermediate values are to be -obtained by an interpolation process that is provided by the implementation. - -(Intuitively, `Clone` is required by `Interpolable` because at the endpoints 0 and 1, clones of the starting and -ending points should be returned. Frequently, `Interpolable` data will actually be `Copy`.) - -For example, Bevy's vector types (including `f32`, `Vec2`, `Vec3`, etc.) implement `Interpolable` using linear -interpolation (the `lerp` function here): -```rust -impl Interpolable for T -where - T: VectorSpace, -{ - fn interpolate(&self, other: &Self, t: f32) -> Self { - self.lerp(*other, t) - } -} -``` -Other forms of interpolation are possible; for example, given two points and tangent vectors at each of -them, one might perform Hermite interpolation to create a cubic curve between them, whose points and tangent -values can then be sampled at any point on the curve. Another common example would be "spherical linear -interpolation" (`slerp`) of quaternions, used in animation and other rigid motions. - -To make using `Interpolable` easier, it has a blanket implementation for tuples whose members are -`Interpolable` (which simultaneously interpolate each constituent); in the same way, the `Interpolable` trait -can be derived for structs whose members are `Interpolable` with `#[derive(Interpolable)]`: -```rust -impl Interpolable for (S, T) -where - S: Interpolable, - T: Interpolable, -{ //... } -//... And so on for (S, T, U), etc. -``` -```rust -#[derive(Interpolable)] -struct MyCurveData { - position: Vec3, - velocity: Vec3, -} -``` - ### Intervals -The `Interval` associated type is the other part of the definition of each `Curve`. This is a type which +The `Interval` type plays a part of the definition of each `Curve`. This is a type which represents a nonempty closed interval over the `f32` numbers whose endpoints may be at infinity. Its provided methods are mostly self-explanatory: ```rust @@ -169,6 +115,13 @@ pub fn clamp(self, value: f32) -> f32 { //... } /// Get the linear map which maps this curve onto the `other` one. Returns an error if either /// interval is infinite. pub fn linear_map_to(self, other: Self) -> Result f32, InfiniteIntervalError> { //... } + +/// Get an iterator over equally-spaced points from this interval in increasing order. +/// Returns an error if `points` is less than 2 or if the interval is unbounded. +pub fn spaced_points( + self, + points: usize, +) -> Result, SpacedPointsError> { ``` The `Interval` type also implements `TryFrom`, which may be desirable if you want to use @@ -201,7 +154,6 @@ We will explore its main components one-by-one. The first of those is `map`: fn map(self, f: impl Fn(T) -> S) -> impl Curve where Self: Sized, - S: Interpolable, { //... } ``` As you can see, `map` takes our curve and, consuming it, produces a new curve whose sample values are the sample @@ -209,7 +161,7 @@ values of the starting curve mapped through `f`. For example, if we started with and wanted to project it onto the XY-plane, we could do that with `map`: ```rust // A 3d curve, implementing `Curve` -let my_3d_curve = function_curve(2.0, |t| Vec3::new(t * t, 2.0 * t, t - 1.0)); +let my_3d_curve = function_curve(interval(0.0, 2.0).unwrap(), |t| Vec3::new(t * t, 2.0 * t, t - 1.0)); // Its 2d projection, implementing `Curve` let my_2d_curve = my_3d_curve.map(|v| Vec2::new(vec.x, vec.y)); ``` @@ -218,7 +170,7 @@ evaluated when the resulting curve is actually sampled. --- -Next up is `reparametrize`, one of the most important API methods: +Next up is `reparametrize`, another of the most important API methods: ```rust /// Create a new [`Curve`] whose parameter space is related to the parameter space of this curve /// by `f`. For each time `t`, the sample from the new curve at time `t` is the sample from @@ -238,9 +190,9 @@ let my_curve = function_curve(interval(0.0, 1.0).unwrap(), |x| x + 1.0); let domain = my_curve.domain(); let scaled_curve = my_curve.reparametrize(interval(0.0, 2.0).unwrap(), |t| t / 2.0); ``` -(A convenience method `reparametrize_linear` exists for this specific thing.) +(A convenience method `reparametrize_linear` exists for this specific kind of thing.) -However, `reparametrize` is vastly more powerful than just this. For example, here we use `reparametrize` to +However, `reparametrize` can do much more than this alone. For example, here we use `reparametrize` to create a new curve that is a segment of the original one: ```rust let my_curve = function_curve(interval(0.0, 1.0).unwrap(), |x| x * 2.0); @@ -299,43 +251,91 @@ Another useful API method is `zip`, which behaves much like its `Iterator` count fn zip(self, other: C) -> Result, InvalidIntervalError> { //... } ``` ---- +### Sampling, resampling, and interpolation + +The next part of the API concerns itself with more imperative (i.e. data-focused) matters. + +Often, one will want to define a curve using some kind of interpolation over discrete data or, conversely, +extract lists of samples that approximate a curve, or convert a curve to one which has been discretized. -The next two API methods concern themselves with more imperative (i.e. data-focused) matters. Often one needs to -take a curve and actually render it concrete by sampling it at some resolution — for the purposes of storage, -serialization, application of numerical methods, and so on. That's where `resample` comes in: +For the first of these, there is the type `SampleCurve`, whose constructor looks like this: ```rust -/// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally -/// spaced values. A total of `samples` samples are used, although at least two samples are -/// required in order to produce well-formed output. If fewer than two samples are provided, -/// or if this curve has an unbounded domain, then a [`ResamplingError`] is returned. -fn resample(&self, samples: usize) -> Result, ResamplingError> { //... } +/// Create a new `SampleCurve`, using the specified `interpolation` to interpolate between +/// the given `samples`. An error is returned if there are not at least 2 samples or if the +/// given `domain` is unbounded. +/// +/// The interpolation takes two values by reference together with a scalar parameter and +/// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and +/// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. +pub fn new( + domain: Interval, + samples: impl Into>, + interpolation: I, +) -> Result +where + I: Fn(&T, &T, f32) -> T, +{ //... } ``` -The main thing to notice about this is that, unlike the previous functional methods which merely spit out some kind -of `impl Curve`, `resample` has a concrete return type of `SampleCurve`, which is an actual struct that holds -actual data that you can access. +Here, the `samples` become a `Vec` whose elements represent equally-spaced sample values over the given +`domain`. Adjacent samples are interpolated using the given `interpolation`. The result is a type that +implements `Curve`, on which the preceding operations can be performed. -Notably, `SampleCurve` still implements `Curve`, but its curve implementation is set in stone: it uses -equally spaced sample points together with the interpolation provided by `T` to provide something which is loosely -equivalent to your original curve, perhaps with a loss of fine detail. The sampling resolution is controlled by -the `samples` parameter, so higher values will ensure something that closely matches your curve. - -For example, with a `Curve` whose `T` performs linear interpolation, a `samples` value of 2 will yield a -`SampleCurve` that represents a line passing between the two endpoints, and as `samples` grows larger, -the original curve is approximated by greater and greater quantities of line segments equally spaced in its parameter -domain. +Conversely, if one wants discrete samples from a curve, there is the function `samples`: +```rust +/// Extract an iterator over evenly-spaced samples from this curve. If `samples` is less than 2 +/// or if this curve has unbounded domain, then an error is returned instead. +fn samples(&self, samples: usize) -> Result, ResamplingError> { //... } +``` -Commonly, `resample` is used to obtain something more concrete after applying the `map` and `reparametrize` methods: +Timed samples can be achieved by an application of `graph`: ```rust -let my_curve = function_curve(interval(0.0, 3.0).unwrap(), |x| x * 2.0 + 1.0); -let modified_curve = my_curve.reparametrize(interval(0.0, 1.0).unwrap(), |t| 3.0 * t).unwrap(); -for (t, v) in modified_curve.graph().resample(100).sample_iter() { - println!("Value of {v} at time {t}"); +for (t, v) in my_curve.by_ref().graph().samples(100).unwrap() { + println!("Value {v} at time {t}."); } ``` -A variant of `resample` called `resample_uneven` allows for choosing the sample points directly instead of having them -evenly spaced: +Finally, the preceding two operations can be combined in one fell swoop with the method `resample`, resulting +in a curve that approximates the original curve using discrete samples: + +```rust +/// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally +/// spaced values, using the provided `interpolation` to interpolate between adjacent samples. +/// A total of `samples` samples are used, although at least two samples are required to produce +/// well-formed output. If fewer than two samples are provided, or if this curve has an unbounded +/// domain, then a [`ResamplingError`] is returned. +fn resample( + &self, + samples: usize, + interpolation: I, +) -> Result, ResamplingError> +where + Self: Sized, + I: Fn(&T, &T, f32) -> T, +{ //... } +``` + +This story has a parallel in `UnevenSampleCurve`, which behaves more like keyframes in that the samples need not be evenly spaced: +```rust +/// Create a new [`UnevenSampleCurve`] using the provided `interpolation` to interpolate +/// between adjacent `timed_samples`. The given samples are filtered to finite times and +/// sorted internally; if there are not at least 2 valid timed samples, an error will be +/// returned. +/// +/// The interpolation takes two values by reference together with a scalar parameter and +/// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and +/// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. +pub fn new( + timed_samples: impl Into>, + interpolation: I, +) -> Result { //... } +``` + +This kind of construction can be useful for strategically saving space when performing serialization and storage (although, keep in mind +that the interpolating function may need to be separately handled in user-space). The `UnevenSampleCurve` type also supports +*forward* mapping of its sample times via the method `map_sample_times`, which accepts a function like the inverse of the one that +would be used in `Curve::reparametrize`. + +The associated resampling method is `resample_uneven`: ```rust /// Resample this [`Curve`] to produce a new one that is defined by interpolation over samples /// taken at the given set of times. The given `sample_times` are expected to contain at least @@ -352,31 +352,42 @@ fn resample_uneven( sample_times: impl IntoIterator, ) -> Result, ResamplingError> { //... } ``` -This is useful for strategically saving space when performing serialization and storage; uneven samples are something -like animation keyframes. The `UnevenSampleCurve` type also supports *forward* mapping of its sample times via -the method `map_sample_times`, which accepts a function like the inverse of the one that would be used in -`Curve::reparametrize`. -### Making curves +Note that there is no equivalent to `samples` for uneven spacing; however, these can be obtained easily using `Iterator::map`: +```rust +for (t, v) in (0..100).map(|x| x * x).map(|t| (t, my_curve.sample(t))) { + println!("Value {v} at time {t}."); +} +``` +The reason for this shortfall is that user expectations surrounding filtering, sorting, clamping, and so on are hard to predict. For example, +`sample` here could be replaced with `sample_clamped`, or the second `map` with `filter_map` and `sample` with `sample_checked`, among many other +variations. With time, perhaps a good default will become apparent and be added to the API. + +Finally, for some common math types whose interpolation is especially obvious and well-behaved (and enshrined in a trait), there are +convenience methods `resample_auto` and `resample_uneven_auto` that use this type-inferred interpolation automatically. The expectation is not +that interpolation in user-space will commonly use this trait and the associated methods, since the bar for having a valid trait implementation +is rather high; rather, in domains where weaker interpolation notions are prevalent (e.g. animation), the expectation is that consumers will +primarily go through `SampleCurve` or `UnevenSampleCurve` or define their own curve constructions entirely. -The curve-creation functions `constant_curve` and `function_curve` that we have been using in examples are, in fact, +### Other ways of making curves + +The curve-creation functions `constant_curve` and `function_curve` that we have been using in examples are in fact real functions that are part of the library. They look like this: ```rust /// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. -pub fn constant_curve(domain: Interval, value: T) -> impl Curve { //... } +pub fn constant_curve(domain: Interval, value: T) -> impl Curve { //... } ``` ```rust /// Convert the given function `f` into a [`Curve`] with the given `domain`, sampled by /// evaluating the function. pub fn function_curve(domain: Interval, f: F) -> impl Curve where - T: Interpolable, F: Fn(f32) -> T, { //... } ``` -Note that, while the examples used only functions `f32 -> f32`, `function_curve` can convert any function of the -parameter domain (valued in something `Interpolable`) into a `Curve`. For example, here is a rotation over time, -expressed as a `Curve` using this API: +Note that, while the examples used mostly functions `f32 -> f32`, `function_curve` can convert any function of the +parameter domain into a `Curve`. For example, here is a rotation over time, expressed as a `Curve` using this +API: ```rust let rotation_curve = function_curve(interval(0.0, std::f32::consts::TAU).unwrap(), |t| Quat::from_rotation_z(t)); ``` @@ -394,14 +405,14 @@ One other minor point is that you may not always want functions like `map` and ` the input curve. For example, the following code takes ownership of `my_curve`, so it cannot be reused, even though `resample` requires only a reference: ```rust -let mapped_sample_curve = my_curve.map(|x| x * 2.0).resample(100).unwrap(); +let mapped_sample_curve = my_curve.map(|x| x * 2.0).resample_auto(100).unwrap(); ``` The `by_ref` method exists to circumvent this problem, allowing curve data to be used through borrowing; this is supported by a blanket implementation which allows any type which dereferences to a `Curve` to be a `Curve` itself. For example, the following are equivalent and do not take ownership of `my_curve`: ```rust -let mapped_sample_curve = my_curve.by_ref().map(|x| x * 2.0).resample(100).unwrap(); -let mapped_sample_curve = (&my_curve).map(|x| x * 2.0).resample(100).unwrap(); +let mapped_sample_curve = my_curve.by_ref().map(|x| x * 2.0).resample_auto(100).unwrap(); +let mapped_sample_curve = (&my_curve).map(|x| x * 2.0).resample_auto(100).unwrap(); ``` ## Implementation strategy @@ -417,8 +428,6 @@ by `MapCurve`, which looks like this: /// given function. pub struct MapCurve where - S: Interpolable, - T: Interpolable, C: Curve, F: Fn(S) -> T, { @@ -429,8 +438,6 @@ where impl Curve for MapCurve where - S: Interpolable, - T: Interpolable, C: Curve, F: Fn(S) -> T, { @@ -452,42 +459,19 @@ in addition to the reparametrization function. The function `graph` is powered b must also own its source data; however, it doesn't require any function information, so it is essentially a plain wrapper struct on the level of data (providing only a different implementation of `Curve::sample`). -A minor point is that `MapCurve` and `ReparamCurve` support special implementations of `map` and `reparametrize`, -which allow data reuse instead of repeatedly wrapping data when repeated calls to these functional APIs are made. -For example, the `map` implementation of `MapCurve` looks like this: -```rust -fn map(self, g: impl Fn(T) -> R) -> impl Curve -where - Self: Sized, - R: Interpolable, -{ - let gf = move |x| g((self.f)(x)); - MapCurve { - preimage: self.preimage, - f: gf, - _phantom: PhantomData, - } -} -``` - -Furthermore, when `reparametrize` and `map` are intermixed, they produce another data structure, `MapReparamCurve`, -which is capable of absorbing all future calls to `map` and `reparametrize` in a way essentially identical to that -demonstrated by the preceding code. - --- -On the other hand, the "concrete" part of the API consists of `SampleCurve`, `UnevenSampleCurve`, and the +On the other hand, the "concrete" part of the API consists of `SampleCurve`, `UnevenSampleCurve`, and the methods that yield them. These are implemented essentially as one would imagine, holding vectors of data and interpolating it in `Curve::sample`. E.g.: ```rust /// A [`Curve`] that is defined by neighbor interpolation over a set of samples. -pub struct SampleCurve -where - T: Interpolable, +pub struct SampleCurve { domain: Interval, samples: Vec, + interpolation: I } // ... @@ -502,18 +486,18 @@ where let lower_index = (t_shifted / step).floor() as usize; let upper_index = (t_shifted / step).ceil() as usize; let f = (t_shifted / step).fract(); - self.samples[lower_index].interpolate(&self.samples[upper_index], f) + (self.interpolation)(&self.samples[lower_index], &self.samples[upper_index], f) } ``` -The main thing here is that `SampleCurve` is actually returned *by type* from the `resample` API, +The main thing here is that `SampleCurve` is actually returned *by type* from the `resample` API, and it is clearly suitable for serialization and storage in addition to numerical applications. This allows consumers to work flexibly with the functional API and then cast down to concrete values when -they need them. (And of course, `UnevenSampleCurve` is implemented similarly, instead using a binary +they need them. (And of course, `UnevenSampleCurve` is implemented similarly, instead using a binary search in its sequence of time-values to find its interpolation interval.) -Because these types actually store concrete sample data, they have special implementations of `map` -which are not lazy, instead returning values of type `SampleCurve`/`UnevenSampleCurve`. These +Because these types actually store concrete sample data, they have special `map`-like methods +which are not lazy, instead returning values of type `SampleCurve`/`UnevenSampleCurve`. These can be accessed from a type-level API as `map_concrete`, so that the user can avoid erasing the type if it is convenient to do so. The same goes for `graph`; however, because of its contravariance (it is function precomposition), the same cannot be said of `reparametrize`, which maintains its default @@ -521,6 +505,46 @@ functional implementation. The latter gap is filled partially by `UnevenSampleCu Everything else should be fairly clear based on the user-facing API descriptions. +### Mathematical and especially nice interpolation + +It is natural to ask what kind of interpolation is especially well-behaved from the perspective of resampling +operations. One answer is that we should expect that the interpolation have *subdivision-stability*: if a curve +between `p` and `q` is defined by interpolation, then resampling the curve at intermediate points and using their +interpolation should result in the same curve as the one we started with. This leads, for example, to the following +law (modulo some omitted `unwrap`s): + +* `curve.resample_auto(n).resample_auto(k * n)` is equivalent to `curve.resample_auto(n)` (where `k > 0`). + +Something similar should hold for uneven sample points: + +* If `iter_few` and `iter_many` are `f32`-valued iterators and all of `iter_few`'s values are contained in those of `iter_many`, then: + `curve.resample_uneven_auto(iter_few).resample_uneven_auto(iter_many)` is equivalent to `curve.resample_uneven_auto(iter_few)`. + +That is to say: subsampling has no effect on sample-interpolated curves with values in such types. This motivates a trait; on the +level of syntax, the trait for such things is completely innocuous, carrying only the shape of an interpolation function: +```rust +pub trait StrongInterpolate { + fn interpolate_strong(&self, &Self, t: f32) -> Self; +} +``` + +However, there are strong expectations that come with implementing this: +1. `interpolate_strong(&x, &y, 0.0)` and `interpolate_strong(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. +2. This mode of interpolation should be "obvious" based on the semantics of the type. +3. This interpolation is subdivision-stable: if `original_curve` is the curve formed by interpolation between two + arbitrary points, and `(t0, p)`, `(t1, q)` are two timed samples from this curve, then the curve formed by + interpolation between `p` and `q` must be the unique linear reparametrization of the curve-segment between `t0` and `t1` + in `original_curve`. + +Equivalently, the second condition may be stated as follows: if `iter` is an iterator yielding `f32` values between `0.0` +and `1.0` (and including `0.0` and `1.0`), then `original_curve.resample_uneven(iter)` is equivalent to `original_curve`. + +Note that here, "equivalent" does not mean "taking the same values in the same order" — it means that they actually +produce equivalent samples at any given parameter value. + +This is the trait that is used for `resample_auto` and `resample_uneven_auto`, and it is to be implemented for `VectorSpace` +types using `lerp`, as well as for rotation and direction types using `slerp`. + ### Object safety The `Curve` trait should be object-safe. While the functional `Curve` API methods are automatically @@ -531,14 +555,13 @@ This prevents methods like `map` from being used by `dyn Curve` (which was ho by providing a blanket implementation over `Deref`, pointers to trait objects can still be used as `Curve`. For instance, `Box>`, `Arc>`, etc. all implement `Curve` through this blanket implementation, which means that they can use the default implementations of `map`, `reparametrize`, etc. built -on top of the object-safe core containing `domain` and `sample`. +on top of the dispatchable core containing `domain` and `sample`. This is the reason for including a `?Sized` constraint on the underlying curve in the blanket implementation, which has a signature that looks like this: ```rust impl Curve for D where - T: Interpolable, C: Curve + ?Sized, D: Deref, { //... } @@ -561,7 +584,7 @@ was deemed adequate are as follows: - Curve applications tend to be artistic in nature rather than demanding high precision, so that `f32` is generally good enough. - If strictly necessary, making the change to use generics is straightforward, but unifying around `f32` - is simpler and encourages low-effort interoperation (i.e. reusing data across domains is more likely). + is simpler and lowers the barrier to interoperation. ## Rationale and alternatives From 0646b8cd3065e795e6bd04962a809fc9a9ad25d9 Mon Sep 17 00:00:00 2001 From: Matty Date: Fri, 14 Jun 2024 19:24:24 -0400 Subject: [PATCH 14/22] Add end-to-end composition --- rfcs/80-curve-trait.md | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index 93713336..3e2c470a 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -239,18 +239,6 @@ let new_curve = my_curve.graph().map(|(t, x)| x * (-t).exp2()); Most general operations that one could think up for curves can be achieved by clever combinations of `map` and `reparametrize`, perhaps with a little `graph` sprinkled in. ---- - -Another useful API method is `zip`, which behaves much like its `Iterator` counterpart: -```rust -/// Create a new [`Curve`] by joining this curve together with another. The sample at time `t` -/// in the new curve is `(x, y)`, where `x` is the sample of `self` at time `t` and `y` is the -/// sample of `other` at time `t`. The domain of the new curve is the intersection of the -/// domains of its constituents. If the domain intersection would be empty, an -/// [`InvalidIntervalError`] is returned. -fn zip(self, other: C) -> Result, InvalidIntervalError> { //... } -``` - ### Sampling, resampling, and interpolation The next part of the API concerns itself with more imperative (i.e. data-focused) matters. @@ -369,6 +357,29 @@ that interpolation in user-space will commonly use this trait and the associated is rather high; rather, in domains where weaker interpolation notions are prevalent (e.g. animation), the expectation is that consumers will primarily go through `SampleCurve` or `UnevenSampleCurve` or define their own curve constructions entirely. +### Combining curves + +There are a couple of common ways of combining curves that are supported by the Curve API. The first of these is `compose`, which appends two curves together end-to-end: +```rust +/// Create a new [`Curve`] by composing this curve end-to-end with another, producing another curve +/// with outputs of the same type. The domain of the other curve is translated so that its start +/// coincides with where this curve ends. A [`CompositionError`] is returned if this curve's domain +/// doesn't have a finite right endpoint or if `other`'s domain doesn't have a finite left endpoint. +fn compose(self, other: C) -> Result, CompositionError> { //... } +``` + +This is useful for doing things like joining paths; note, however, that it cannot generally provide any guarantees that the resulting curve doesn't abruptly transition from the first curve to the second. + +The second useful API method in this category is `zip`, which behaves much like its `Iterator` counterpart: +```rust +/// Create a new [`Curve`] by joining this curve together with another. The sample at time `t` +/// in the new curve is `(x, y)`, where `x` is the sample of `self` at time `t` and `y` is the +/// sample of `other` at time `t`. The domain of the new curve is the intersection of the +/// domains of its constituents. If the domain intersection would be empty, an +/// [`InvalidIntervalError`] is returned. +fn zip(self, other: C) -> Result, InvalidIntervalError> { //... } +``` + ### Other ways of making curves The curve-creation functions `constant_curve` and `function_curve` that we have been using in examples are in fact From 6d3ee806239a1d9a3c1547d4a7be0029a1f3ff20 Mon Sep 17 00:00:00 2001 From: Matty Date: Fri, 28 Jun 2024 10:02:57 -0400 Subject: [PATCH 15/22] Added section on shared interpolation interfaces --- rfcs/80-curve-trait.md | 142 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 3 deletions(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index 3e2c470a..4d71c3c2 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -482,7 +482,7 @@ pub struct SampleCurve { domain: Interval, samples: Vec, - interpolation: I + interpolation: I, } // ... @@ -553,8 +553,144 @@ and `1.0` (and including `0.0` and `1.0`), then `original_curve.resample_uneven( Note that here, "equivalent" does not mean "taking the same values in the same order" — it means that they actually produce equivalent samples at any given parameter value. -This is the trait that is used for `resample_auto` and `resample_uneven_auto`, and it is to be implemented for `VectorSpace` -types using `lerp`, as well as for rotation and direction types using `slerp`. +This is the trait that is used for `resample_auto` and `resample_uneven_auto`, and it is to be implemented for `NormedVectorSpace` +types using `lerp`, as well as for rotation and direction types using `slerp`. + +### Shared interpolation interfaces + +Under the hood, the aforementioned `SampleCurve`/`SampleAutoCurve`/`UnevenSampleCurve`/`UnevenSampleAutoCurve` constructions +use shared API backends that abstract away the meat of the data access patterns. For example, evenly-spaced samples use a data +structure that looks like this: + +```rust +pub struct EvenCore { + /// The domain over which the samples are taken, which corresponds to the domain of the curve + /// formed by interpolating them. + /// + /// # Invariants + /// This must always be a bounded interval; i.e. its endpoints must be finite. + pub domain: Interval, + + /// The samples that are interpolated to extract values. + /// + /// # Invariants + /// This must always have a length of at least 2. + pub samples: Vec, +} +``` + +Unevenly-spaced samples have something similar: + +```rust +pub struct UnevenCore { + /// The times for the samples of this curve. + /// + /// # Invariants + /// This must always have a length of at least 2, be sorted, and have no + /// duplicated or non-finite times. + pub times: Vec, + + /// The samples corresponding to the times for this curve. + /// + /// # Invariants + /// This must always have the same length as `times`. + pub samples: Vec, +} +``` + +Notably, these have public fields (despite requiring invariants) because they are intended to be extensible by +authors of libraries and so on; on the other hand, each has a `new` function which enforces the invariants, +returning an error if they are not met. + +The benefit of using these is that the interpolation access patterns are done for you; the methods +`sample_interp` and `sample_interp_timed` do the meat of the interpolation, allowing custom interpolation to +be easily defined. Here is what those look like: + +```rust +/// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover +/// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can +/// be used to interpolate between the two contained values with the given parameter. The other +/// variants give additional context about where the value is relative to the family of samples. +/// +/// [`Between`]: `InterpolationDatum::Between` +pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&T> { //... } +``` + +```rust +/// Like [`sample_interp`], but the returned values include the sample times. This can be +/// useful when sampling is not scale-invariant. +/// +/// [`sample_interp`]: EvenCore::sample_interp +pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> { //... } +``` + +Most of the time, `sample_interp` is sufficient, but `sample_interp_timed` may be required when interpolation +is not scale-invariant. + +`InterpolationDatum` is just an enum that looks like this: +```rust +pub enum InterpolationDatum { + /// This value lies exactly on a value in the family. + Exact(T), + + /// This value is off the left tail of the family; the inner value is the family's leftmost. + LeftTail(T), + + /// This value is off the right tail of the family; the inner value is the family's rightmost. + RightTail(T), + + /// This value lies on the interior, in between two points, with a third parameter expressing + /// the interpolation factor between the two. + Between(T, T, f32), +} +``` + +Here is an example that demonstrates how to use `EvenCore` to create a `Curve` that contains its interpolation mode +in an enum: +```rust +use bevy_math::curve::*; +use bevy_math::curve::builders::*; + +enum InterpolationMode { + Linear, + Step, +} + +trait LinearInterpolate { + fn lerp(&self, other: &Self, t: f32) -> Self; +} + +fn step(first: &T, second: &T, t: f32) -> T { + if t >= 1.0 { + second.clone() + } else { + first.clone() + } +} + +struct MyCurve { + core: SampleCore, + interpolation_mode: InterpolationMode, +} + +impl Curve for MyCurve +where + T: LinearInterpolate + Clone, +{ + fn domain(&self) -> Interval { + self.core.domain() + } + + fn sample(&self, t: f32) -> T { + match self.interpolation_mode { + InterpolationMode::Linear => self.core.sample_with(t, ::lerp), + InterpolationMode::Step => self.core.sample_with(t, step), + } + } +} +``` + +I think this does a pretty good job of demonstrating how the "core" concept can be useful to implementors. ### Object safety From 35d8f424e195cebac1f9f03a70cf0f0fed8b3a6f Mon Sep 17 00:00:00 2001 From: Matty Date: Fri, 28 Jun 2024 11:39:14 -0400 Subject: [PATCH 16/22] Mention sample_with in exposition --- rfcs/80-curve-trait.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index 4d71c3c2..e593b646 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -645,6 +645,10 @@ pub enum InterpolationDatum { } ``` +For simple cases (e.g. creating something which behaves like `SampleCurve`), there is a helper function `sample_with` +which takes an explicit interpolation and does the obvious thing, cloning the value of any case except `Between`, where +the interpolation is used. + Here is an example that demonstrates how to use `EvenCore` to create a `Curve` that contains its interpolation mode in an enum: ```rust From e92062937668d504b129a50ab9a2b99c6ae93f6c Mon Sep 17 00:00:00 2001 From: Matty Date: Wed, 31 Jul 2024 05:29:45 -0400 Subject: [PATCH 17/22] Update rfcs/80-curve-trait.md Co-authored-by: Alice Cecile --- rfcs/80-curve-trait.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index e593b646..e2c32fb6 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -46,7 +46,7 @@ of values of type `T` parametrized over some interval which is typically thought or distance. For example, we might use the `Curve` APIs to construct a `Curve` in order to describe the motion -of an Entity with time: +of an entity over time: ```rust // This is a `Curve` that describes a rotation with time: let rotation_over_time = function_curve(interval(0.0, std::f32::consts::TAU).unwrap(), |t| Quat::from_rotation_z(t)); From c95a400bb203269fac26554b9e59e35b1451bbfc Mon Sep 17 00:00:00 2001 From: Matty Date: Thu, 1 Aug 2024 16:11:03 -0400 Subject: [PATCH 18/22] Rename compose -> chain --- rfcs/80-curve-trait.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index e2c32fb6..8dd513c3 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -359,13 +359,13 @@ primarily go through `SampleCurve` or `UnevenSampleCurve` or define their own cu ### Combining curves -There are a couple of common ways of combining curves that are supported by the Curve API. The first of these is `compose`, which appends two curves together end-to-end: +There are a couple of common ways of combining curves that are supported by the Curve API. The first of these is `chain`, which joins two curves together end-to-end: ```rust -/// Create a new [`Curve`] by composing this curve end-to-end with another, producing another curve +/// Create a new [`Curve`] by joining this curve end-to-end with another, producing another curve /// with outputs of the same type. The domain of the other curve is translated so that its start /// coincides with where this curve ends. A [`CompositionError`] is returned if this curve's domain /// doesn't have a finite right endpoint or if `other`'s domain doesn't have a finite left endpoint. -fn compose(self, other: C) -> Result, CompositionError> { //... } +fn chain(self, other: C) -> Result, CompositionError> { //... } ``` This is useful for doing things like joining paths; note, however, that it cannot generally provide any guarantees that the resulting curve doesn't abruptly transition from the first curve to the second. From 37c3ad9b6cd88fb1390e7dec31bf9b0a08c7aa18 Mon Sep 17 00:00:00 2001 From: Matty Date: Thu, 1 Aug 2024 16:12:42 -0400 Subject: [PATCH 19/22] Updates to Interval methods --- rfcs/80-curve-trait.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index 8dd513c3..cde33612 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -112,16 +112,13 @@ pub fn intersect(self, other: Interval) -> Result f32 { //... } -/// Get the linear map which maps this curve onto the `other` one. Returns an error if either -/// interval is infinite. -pub fn linear_map_to(self, other: Self) -> Result f32, InfiniteIntervalError> { //... } - /// Get an iterator over equally-spaced points from this interval in increasing order. -/// Returns an error if `points` is less than 2 or if the interval is unbounded. +/// If `points` is 1, the start of this interval is returned. If `points` is 0, an empty +/// iterator is returned. pub fn spaced_points( self, points: usize, -) -> Result, SpacedPointsError> { +) -> impl Iterator { ``` The `Interval` type also implements `TryFrom`, which may be desirable if you want to use From 1426a74229fa6adeab75d3c26c16224c97b993e8 Mon Sep 17 00:00:00 2001 From: Matty Date: Thu, 1 Aug 2024 21:07:12 -0400 Subject: [PATCH 20/22] Clarify that Interval is not an Iterator --- rfcs/80-curve-trait.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index cde33612..72b29aa7 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -122,8 +122,9 @@ pub fn spaced_points( ``` The `Interval` type also implements `TryFrom`, which may be desirable if you want to use -the `start..=end` syntax. One of the primary benefits of `Interval` (in addition to these methods) is -that it is `Copy`, so it is easy to take intervals and throw them around. +the `start..=end` syntax. Note, however, that `Interval` is not an iterator. One of the primary benefits of +`Interval` (in addition to these methods) is that it is `Copy` (unlike the range types), so it is easy to +take intervals and throw them around. ### Sampling From a45808552e671063a00319779177a86b3ce7e5c0 Mon Sep 17 00:00:00 2001 From: Matty Date: Thu, 1 Aug 2024 21:09:30 -0400 Subject: [PATCH 21/22] Add Interval::contains_interval for checking containment between two intervals --- rfcs/80-curve-trait.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index 72b29aa7..d032b9f0 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -105,6 +105,9 @@ pub fn is_finite(self) -> bool { //... } /// Returns `true` if `item` is contained in this interval. pub fn contains(self, item: f32) -> bool { //... } +/// Returns `true` if the other interval is contained in this interval (non-strictly). +pub fn contains_interval(self, other: Interval) -> bool { //... } + /// Create an [`Interval`] by intersecting this interval with another. Returns an error if the /// intersection would be empty (hence an invalid interval). pub fn intersect(self, other: Interval) -> Result { //... } From fbe233f04fc5efedae0144f9b5d0fe95011f183e Mon Sep 17 00:00:00 2001 From: Matty Date: Thu, 1 Aug 2024 21:16:12 -0400 Subject: [PATCH 22/22] Switch resample from samples to segments --- rfcs/80-curve-trait.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rfcs/80-curve-trait.md b/rfcs/80-curve-trait.md index d032b9f0..ae901c68 100644 --- a/rfcs/80-curve-trait.md +++ b/rfcs/80-curve-trait.md @@ -288,13 +288,14 @@ in a curve that approximates the original curve using discrete samples: ```rust /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally -/// spaced values, using the provided `interpolation` to interpolate between adjacent samples. -/// A total of `samples` samples are used, although at least two samples are required to produce -/// well-formed output. If fewer than two samples are provided, or if this curve has an unbounded +/// spaced sample values, using the provided `interpolation` to interpolate between adjacent samples. +/// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1, +/// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at +/// the midpoint is taken as well, and so on. If `segments` is zero, or if this curve has an unbounded /// domain, then a [`ResamplingError`] is returned. fn resample( &self, - samples: usize, + segments: usize, interpolation: I, ) -> Result, ResamplingError> where