Skip to main content

bevy_curve/
interval.rs

1//! The [`Interval`] type for nonempty intervals used by the [`Curve`](super::Curve) trait.
2
3use core::{
4    cmp::{max_by, min_by},
5    ops::RangeInclusive,
6};
7use itertools::Either;
8use thiserror::Error;
9
10#[cfg(feature = "bevy_reflect")]
11use bevy_reflect::Reflect;
12#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]
13use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
14
15/// A nonempty closed interval, possibly unbounded in either direction.
16///
17/// In other words, the interval may stretch all the way to positive or negative infinity, but it
18/// will always have some nonempty interior.
19#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
20#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
21#[cfg_attr(
22    feature = "bevy_reflect",
23    derive(Reflect),
24    reflect(Debug, PartialEq, Clone)
25)]
26#[cfg_attr(
27    all(feature = "serialize", feature = "bevy_reflect"),
28    reflect(Serialize, Deserialize)
29)]
30pub struct Interval {
31    start: f32,
32    end: f32,
33}
34
35/// An error that indicates that an operation would have returned an invalid [`Interval`].
36#[derive(Debug, Error)]
37#[error("The resulting interval would be invalid (empty or with a NaN endpoint)")]
38pub struct InvalidIntervalError;
39
40/// An error indicating that spaced points could not be extracted from an unbounded interval.
41#[derive(Debug, Error)]
42#[error("Cannot extract spaced points from an unbounded interval")]
43pub struct SpacedPointsError;
44
45/// An error indicating that a linear map between intervals could not be constructed because of
46/// unboundedness.
47#[derive(Debug, Error)]
48#[error("Could not construct linear function to map between intervals")]
49pub(super) enum LinearMapError {
50    /// The source interval being mapped out of was unbounded.
51    #[error("The source interval is unbounded")]
52    SourceUnbounded,
53
54    /// The target interval being mapped into was unbounded.
55    #[error("The target interval is unbounded")]
56    TargetUnbounded,
57}
58
59impl Interval {
60    /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be unbounded
61    /// but cannot be empty (so `start` must be less than `end`) and neither endpoint can be NaN; invalid
62    /// parameters will result in an error.
63    #[inline]
64    pub const fn new(start: f32, end: f32) -> Result<Self, InvalidIntervalError> {
65        if start >= end || start.is_nan() || end.is_nan() {
66            Err(InvalidIntervalError)
67        } else {
68            Ok(Self { start, end })
69        }
70    }
71
72    /// An interval of length 1.0, starting at 0.0 and ending at 1.0.
73    pub const UNIT: Self = Self {
74        start: 0.0,
75        end: 1.0,
76    };
77
78    /// An interval which stretches across the entire real line from negative infinity to infinity.
79    pub const EVERYWHERE: Self = Self {
80        start: f32::NEG_INFINITY,
81        end: f32::INFINITY,
82    };
83
84    /// Get the start of this interval.
85    #[inline]
86    pub const fn start(self) -> f32 {
87        self.start
88    }
89
90    /// Get the end of this interval.
91    #[inline]
92    pub const fn end(self) -> f32 {
93        self.end
94    }
95
96    /// Create an [`Interval`] by intersecting this interval with another. Returns an error if the
97    /// intersection would be empty (hence an invalid interval).
98    pub fn intersect(self, other: Interval) -> Result<Interval, InvalidIntervalError> {
99        let lower = max_by(self.start, other.start, f32::total_cmp);
100        let upper = min_by(self.end, other.end, f32::total_cmp);
101        Self::new(lower, upper)
102    }
103
104    /// Get the length of this interval. Note that the result may be infinite (`f32::INFINITY`).
105    #[inline]
106    pub const fn length(self) -> f32 {
107        self.end - self.start
108    }
109
110    /// Returns `true` if this interval is bounded — that is, if both its start and end are finite.
111    ///
112    /// Equivalently, an interval is bounded if its length is finite.
113    #[inline]
114    pub const fn is_bounded(self) -> bool {
115        self.length().is_finite()
116    }
117
118    /// Returns `true` if this interval has a finite start.
119    #[inline]
120    pub const fn has_finite_start(self) -> bool {
121        self.start.is_finite()
122    }
123
124    /// Returns `true` if this interval has a finite end.
125    #[inline]
126    pub const fn has_finite_end(self) -> bool {
127        self.end.is_finite()
128    }
129
130    /// Returns `true` if `item` is contained in this interval.
131    #[inline]
132    pub fn contains(self, item: f32) -> bool {
133        (self.start..=self.end).contains(&item)
134    }
135
136    /// Returns `true` if the other interval is contained in this interval.
137    ///
138    /// This is non-strict: each interval will contain itself.
139    #[inline]
140    pub const fn contains_interval(self, other: Self) -> bool {
141        self.start <= other.start && self.end >= other.end
142    }
143
144    /// Clamp the given `value` to lie within this interval.
145    #[inline]
146    pub const fn clamp(self, value: f32) -> f32 {
147        value.clamp(self.start, self.end)
148    }
149
150    /// Get an iterator over equally-spaced points from this interval in increasing order.
151    /// If `points` is 1, the start of this interval is returned. If `points` is 0, an empty
152    /// iterator is returned. An error is returned if the interval is unbounded.
153    #[inline]
154    pub fn spaced_points(
155        self,
156        points: usize,
157    ) -> Result<impl Iterator<Item = f32>, SpacedPointsError> {
158        if !self.is_bounded() {
159            return Err(SpacedPointsError);
160        }
161        if points < 2 {
162            // If `points` is 1, this is `Some(self.start)` as an iterator, and if `points` is 0,
163            // then this is `None` as an iterator. This is written this way to avoid having to
164            // introduce a ternary disjunction of iterators.
165            let iter = (points == 1).then_some(self.start).into_iter();
166            return Ok(Either::Left(iter));
167        }
168        let step = self.length() / (points - 1) as f32;
169        let iter = (0..points).map(move |x| self.start + x as f32 * step);
170        Ok(Either::Right(iter))
171    }
172
173    /// Get the linear function which maps this interval onto the `other` one. Returns an error if either
174    /// interval is unbounded.
175    #[inline]
176    pub(super) const fn linear_map_to(
177        self,
178        other: Self,
179    ) -> Result<impl Fn(f32) -> f32, LinearMapError> {
180        if !self.is_bounded() {
181            return Err(LinearMapError::SourceUnbounded);
182        }
183
184        if !other.is_bounded() {
185            return Err(LinearMapError::TargetUnbounded);
186        }
187
188        let scale = other.length() / self.length();
189        Ok(move |x| (x - self.start) * scale + other.start)
190    }
191}
192
193impl TryFrom<RangeInclusive<f32>> for Interval {
194    type Error = InvalidIntervalError;
195    fn try_from(range: RangeInclusive<f32>) -> Result<Self, Self::Error> {
196        Interval::new(*range.start(), *range.end())
197    }
198}
199
200/// Create an [`Interval`] with a given `start` and `end`. Alias of [`Interval::new`].
201#[inline]
202pub const fn interval(start: f32, end: f32) -> Result<Interval, InvalidIntervalError> {
203    Interval::new(start, end)
204}
205
206#[cfg(test)]
207mod tests {
208    use bevy_math::ops;
209
210    use super::*;
211    use alloc::vec::Vec;
212    use approx::{assert_abs_diff_eq, AbsDiffEq};
213
214    #[test]
215    fn make_intervals() {
216        let ivl = Interval::new(2.0, -1.0);
217        assert!(ivl.is_err());
218
219        let ivl = Interval::new(-0.0, 0.0);
220        assert!(ivl.is_err());
221
222        let ivl = Interval::new(f32::NEG_INFINITY, 15.5);
223        assert!(ivl.is_ok());
224
225        let ivl = Interval::new(-2.0, f32::INFINITY);
226        assert!(ivl.is_ok());
227
228        let ivl = Interval::new(f32::NEG_INFINITY, f32::INFINITY);
229        assert!(ivl.is_ok());
230
231        let ivl = Interval::new(f32::INFINITY, f32::NEG_INFINITY);
232        assert!(ivl.is_err());
233
234        let ivl = Interval::new(-1.0, f32::NAN);
235        assert!(ivl.is_err());
236
237        let ivl = Interval::new(f32::NAN, -42.0);
238        assert!(ivl.is_err());
239
240        let ivl = Interval::new(f32::NAN, f32::NAN);
241        assert!(ivl.is_err());
242
243        let ivl = Interval::new(0.0, 1.0);
244        assert!(ivl.is_ok());
245    }
246
247    #[test]
248    fn lengths() {
249        let ivl = interval(-5.0, 10.0).unwrap();
250        assert!(ops::abs(ivl.length() - 15.0) <= f32::EPSILON);
251
252        let ivl = interval(5.0, 100.0).unwrap();
253        assert!(ops::abs(ivl.length() - 95.0) <= f32::EPSILON);
254
255        let ivl = interval(0.0, f32::INFINITY).unwrap();
256        assert_eq!(ivl.length(), f32::INFINITY);
257
258        let ivl = interval(f32::NEG_INFINITY, 0.0).unwrap();
259        assert_eq!(ivl.length(), f32::INFINITY);
260
261        let ivl = Interval::EVERYWHERE;
262        assert_eq!(ivl.length(), f32::INFINITY);
263    }
264
265    #[test]
266    fn intersections() {
267        let ivl1 = interval(-1.0, 1.0).unwrap();
268        let ivl2 = interval(0.0, 2.0).unwrap();
269        let ivl3 = interval(-3.0, 0.0).unwrap();
270        let ivl4 = interval(0.0, f32::INFINITY).unwrap();
271        let ivl5 = interval(f32::NEG_INFINITY, 0.0).unwrap();
272        let ivl6 = Interval::EVERYWHERE;
273
274        assert!(ivl1.intersect(ivl2).is_ok_and(|ivl| ivl == Interval::UNIT));
275        assert!(ivl1
276            .intersect(ivl3)
277            .is_ok_and(|ivl| ivl == interval(-1.0, 0.0).unwrap()));
278        assert!(ivl2.intersect(ivl3).is_err());
279        assert!(ivl1.intersect(ivl4).is_ok_and(|ivl| ivl == Interval::UNIT));
280        assert!(ivl1
281            .intersect(ivl5)
282            .is_ok_and(|ivl| ivl == interval(-1.0, 0.0).unwrap()));
283        assert!(ivl4.intersect(ivl5).is_err());
284        assert_eq!(ivl1.intersect(ivl6).unwrap(), ivl1);
285        assert_eq!(ivl4.intersect(ivl6).unwrap(), ivl4);
286        assert_eq!(ivl5.intersect(ivl6).unwrap(), ivl5);
287    }
288
289    #[test]
290    fn containment() {
291        let ivl = Interval::UNIT;
292        assert!(ivl.contains(0.0));
293        assert!(ivl.contains(1.0));
294        assert!(ivl.contains(0.5));
295        assert!(!ivl.contains(-0.1));
296        assert!(!ivl.contains(1.1));
297        assert!(!ivl.contains(f32::NAN));
298
299        let ivl = interval(3.0, f32::INFINITY).unwrap();
300        assert!(ivl.contains(3.0));
301        assert!(ivl.contains(2.0e5));
302        assert!(ivl.contains(3.5e6));
303        assert!(!ivl.contains(2.5));
304        assert!(!ivl.contains(-1e5));
305        assert!(!ivl.contains(f32::NAN));
306    }
307
308    #[test]
309    fn interval_containment() {
310        let ivl = Interval::UNIT;
311        assert!(ivl.contains_interval(interval(-0.0, 0.5).unwrap()));
312        assert!(ivl.contains_interval(interval(0.5, 1.0).unwrap()));
313        assert!(ivl.contains_interval(interval(0.25, 0.75).unwrap()));
314        assert!(!ivl.contains_interval(interval(-0.25, 0.5).unwrap()));
315        assert!(!ivl.contains_interval(interval(0.5, 1.25).unwrap()));
316        assert!(!ivl.contains_interval(interval(0.25, f32::INFINITY).unwrap()));
317        assert!(!ivl.contains_interval(interval(f32::NEG_INFINITY, 0.75).unwrap()));
318
319        let big_ivl = interval(0.0, f32::INFINITY).unwrap();
320        assert!(big_ivl.contains_interval(interval(0.0, 5.0).unwrap()));
321        assert!(big_ivl.contains_interval(interval(0.0, f32::INFINITY).unwrap()));
322        assert!(big_ivl.contains_interval(interval(1.0, 5.0).unwrap()));
323        assert!(!big_ivl.contains_interval(interval(-1.0, f32::INFINITY).unwrap()));
324        assert!(!big_ivl.contains_interval(interval(-2.0, 5.0).unwrap()));
325    }
326
327    #[test]
328    fn boundedness() {
329        assert!(!Interval::EVERYWHERE.is_bounded());
330        assert!(interval(0.0, 3.5e5).unwrap().is_bounded());
331        assert!(!interval(-2.0, f32::INFINITY).unwrap().is_bounded());
332        assert!(!interval(f32::NEG_INFINITY, 5.0).unwrap().is_bounded());
333    }
334
335    #[test]
336    fn linear_maps() {
337        let ivl1 = interval(-3.0, 5.0).unwrap();
338        let ivl2 = Interval::UNIT;
339        let map = ivl1.linear_map_to(ivl2);
340        assert!(map.is_ok_and(|f| f(-3.0).abs_diff_eq(&0.0, f32::EPSILON)
341            && f(5.0).abs_diff_eq(&1.0, f32::EPSILON)
342            && f(1.0).abs_diff_eq(&0.5, f32::EPSILON)));
343
344        let ivl1 = Interval::UNIT;
345        let ivl2 = Interval::EVERYWHERE;
346        assert!(ivl1.linear_map_to(ivl2).is_err());
347
348        let ivl1 = interval(f32::NEG_INFINITY, -4.0).unwrap();
349        let ivl2 = Interval::UNIT;
350        assert!(ivl1.linear_map_to(ivl2).is_err());
351    }
352
353    #[test]
354    fn spaced_points() {
355        let ivl = interval(0.0, 50.0).unwrap();
356        let points_iter: Vec<f32> = ivl.spaced_points(1).unwrap().collect();
357        assert_abs_diff_eq!(points_iter[0], 0.0);
358        assert_eq!(points_iter.len(), 1);
359        let points_iter: Vec<f32> = ivl.spaced_points(2).unwrap().collect();
360        assert_abs_diff_eq!(points_iter[0], 0.0);
361        assert_abs_diff_eq!(points_iter[1], 50.0);
362        let points_iter = ivl.spaced_points(21).unwrap();
363        let step = ivl.length() / 20.0;
364        for (index, point) in points_iter.enumerate() {
365            let expected = ivl.start() + step * index as f32;
366            assert_abs_diff_eq!(point, expected);
367        }
368
369        let ivl = interval(-21.0, 79.0).unwrap();
370        let points_iter = ivl.spaced_points(10000).unwrap();
371        let step = ivl.length() / 9999.0;
372        for (index, point) in points_iter.enumerate() {
373            let expected = ivl.start() + step * index as f32;
374            assert_abs_diff_eq!(point, expected);
375        }
376
377        let ivl = interval(-1.0, f32::INFINITY).unwrap();
378        let points_iter = ivl.spaced_points(25);
379        assert!(points_iter.is_err());
380
381        let ivl = interval(f32::NEG_INFINITY, -25.0).unwrap();
382        let points_iter = ivl.spaced_points(9);
383        assert!(points_iter.is_err());
384    }
385}