From 96f1fd73cb73246f3c04748699818b0e58c490df Mon Sep 17 00:00:00 2001 From: Robert Walter <26892280+RobWalt@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:08:41 +0000 Subject: [PATCH] Add methods to sample curves from `IntoIterator` types (#14815) # Objective Citing @mweatherley > As mentioned before, a multi-sampling function in the API which takes an iterator is probably something we want (e.g. `sample_iter(iter: impl IntoIterator) -> impl IntoIterator { //... }`, but there are some design choices to be made on the details (e.g. does this filter out points that aren't in the domain? does it do sorting? etc.) ## Solution I think the most flexible solution for end users is to expose all the `sample_...` functions with an `iter` equivalent, so we'll have - `sample_iter` - `sample_iter_unchecked` - `sample_iter_clamped` Answering some questions from the original idea: > does this filter out points that aren't in the domain? With the methods the user has the choice to just sample or if they want to filter out invalid types us `sample_iter` and then apply `filter_map` to the iterator returned themselves. > does it do sorting? I think it's the same thing. If the user wants it, they need to do it themselves by either collecting and sorting a `Vec` or using `itertools`. I think there is a legit use case for "please sample me this collection of points that are unordered" and we would destroy it if we take away to much agency from users by sorting for them ## Testing - Added a test which covers all three methods --- crates/bevy_math/src/curve/mod.rs | 72 +++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 03cfd7c74c..4440fb72ed 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -51,6 +51,44 @@ pub trait Curve { self.sample_unchecked(t) } + /// Sample a collection of `n >= 0` points on this curve at the parameter values `t_n`, + /// returning `None` if the point is outside of the curve's domain. + /// + /// The samples are returned in the same order as the parameter values `t_n` were provided and + /// will include all results. This leaves the responsibility for things like filtering and + /// sorting to the user for maximum flexibility. + fn sample_iter(&self, iter: impl IntoIterator) -> impl Iterator> { + iter.into_iter().map(|t| self.sample(t)) + } + + /// Sample a collection of `n >= 0` points on this curve at the parameter values `t_n`, + /// extracting the associated values. This is the unchecked version of sampling, which should + /// only be used if the sample times `t_n` are already known to lie within the curve's domain. + /// + /// Values sampled from outside of a curve's domain are generally considered invalid; data + /// which is nonsensical or otherwise useless may be returned in such a circumstance, and + /// extrapolation beyond a curve's domain should not be relied upon. + /// + /// The samples are returned in the same order as the parameter values `t_n` were provided and + /// will include all results. This leaves the responsibility for things like filtering and + /// sorting to the user for maximum flexibility. + fn sample_iter_unchecked( + &self, + iter: impl IntoIterator, + ) -> impl Iterator { + iter.into_iter().map(|t| self.sample_unchecked(t)) + } + + /// Sample a collection of `n >= 0` points on this curve at the parameter values `t_n`, + /// clamping `t_n` to lie inside the domain of the curve. + /// + /// The samples are returned in the same order as the parameter values `t_n` were provided and + /// will include all results. This leaves the responsibility for things like filtering and + /// sorting to the user for maximum flexibility. + fn sample_iter_clamped(&self, iter: impl IntoIterator) -> impl Iterator { + iter.into_iter().map(|t| self.sample_clamped(t)) + } + /// 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)`. @@ -1124,4 +1162,38 @@ mod tests { assert_abs_diff_eq!(resampled_curve.domain().start(), 1.0); assert_abs_diff_eq!(resampled_curve.domain().end(), 512.0); } + + #[test] + fn sample_iterators() { + let times = [-0.5, 0.0, 0.5, 1.0, 1.5]; + + let curve = function_curve(Interval::EVERYWHERE, |t| t * 3.0 + 1.0); + let samples = curve.sample_iter_unchecked(times).collect::>(); + let [y0, y1, y2, y3, y4] = samples.try_into().unwrap(); + + assert_eq!(y0, -0.5 * 3.0 + 1.0); + assert_eq!(y1, 0.0 * 3.0 + 1.0); + assert_eq!(y2, 0.5 * 3.0 + 1.0); + assert_eq!(y3, 1.0 * 3.0 + 1.0); + assert_eq!(y4, 1.5 * 3.0 + 1.0); + + let finite_curve = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); + let samples = finite_curve.sample_iter(times).collect::>(); + let [y0, y1, y2, y3, y4] = samples.try_into().unwrap(); + + assert_eq!(y0, None); + assert_eq!(y1, Some(0.0 * 3.0 + 1.0)); + assert_eq!(y2, Some(0.5 * 3.0 + 1.0)); + assert_eq!(y3, Some(1.0 * 3.0 + 1.0)); + assert_eq!(y4, None); + + let samples = finite_curve.sample_iter_clamped(times).collect::>(); + let [y0, y1, y2, y3, y4] = samples.try_into().unwrap(); + + assert_eq!(y0, 0.0 * 3.0 + 1.0); + assert_eq!(y1, 0.0 * 3.0 + 1.0); + assert_eq!(y2, 0.5 * 3.0 + 1.0); + assert_eq!(y3, 1.0 * 3.0 + 1.0); + assert_eq!(y4, 1.0 * 3.0 + 1.0); + } }