• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

djeedai / bevy_hanabi / 17622440846

10 Sep 2025 05:55PM UTC coverage: 66.033% (-0.6%) from 66.641%
17622440846

push

github

web-flow
Fixes for rustc v1.89 (#494)

Works around a bug in `encase` :
https://github.com/teoxoy/encase/issues/95

9 of 17 new or added lines in 7 files covered. (52.94%)

133 existing lines in 10 files now uncovered.

4829 of 7313 relevant lines covered (66.03%)

437.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.57
/src/gradient.rs
1
use std::hash::{Hash, Hasher};
2

3
use bevy::{
4
    math::{FloatOrd, Quat, Vec2, Vec3, Vec3A, Vec4},
5
    reflect::{FromReflect, Reflect},
6
};
7
use serde::{Deserialize, Serialize};
8

9
/// Describes a type that can be linearly interpolated between two keys.
10
///
11
/// This trait is used for values in a gradient, which are primitive types and
12
/// are therefore copyable.
13
pub trait Lerp: Copy {
14
    fn lerp(self, other: Self, ratio: f32) -> Self;
15
}
16

17
impl Lerp for f32 {
18
    #[inline]
19
    fn lerp(self, other: Self, ratio: f32) -> Self {
6✔
20
        self.mul_add(1. - ratio, other * ratio)
24✔
21
    }
22
}
23

24
impl Lerp for f64 {
25
    #[inline]
26
    fn lerp(self, other: Self, ratio: f32) -> Self {
6✔
27
        self.mul_add((1. - ratio) as f64, other * ratio as f64)
24✔
28
    }
29
}
30

31
macro_rules! impl_lerp_vecn {
32
    ($t:ty) => {
33
        impl Lerp for $t {
34
            #[inline]
35
            fn lerp(self, other: Self, ratio: f32) -> Self {
155✔
36
                // Force use of type's own lerp() to disambiguate and prevent infinite recursion
37
                <$t>::lerp(self, other, ratio)
620✔
38
            }
39
        }
40
    };
41
}
42

43
impl_lerp_vecn!(Vec2);
44
impl_lerp_vecn!(Vec3);
45
impl_lerp_vecn!(Vec3A);
46
impl_lerp_vecn!(Vec4);
47

48
impl Lerp for Quat {
49
    fn lerp(self, other: Self, ratio: f32) -> Self {
6✔
50
        // We use slerp() instead of lerp() as conceptually we want a smooth
51
        // interpolation and we expect Quat to be used to represent a rotation.
52
        // lerp() would produce an interpolation with varying speed, which feels
53
        // non-natural.
54
        self.slerp(other, ratio)
24✔
55
    }
56
}
57

58
/// A single key point for a [`Gradient`].
59
#[derive(Debug, Default, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
60
pub struct GradientKey<T: Lerp + FromReflect> {
61
    /// Ratio in \[0:1\] where the key is located.
62
    ratio: f32,
63

64
    /// Value associated with the key.
65
    ///
66
    /// The value is uploaded as is to the render shader. For colors, this means
67
    /// the value does not imply any particular color space by itself.
68
    pub value: T,
69
}
70

71
impl<T: Lerp + FromReflect> GradientKey<T> {
72
    /// Get the ratio where the key point is located, in \[0:1\].
73
    pub fn ratio(&self) -> f32 {
14✔
74
        self.ratio
14✔
75
    }
76
}
77

78
impl Hash for GradientKey<f32> {
79
    fn hash<H: Hasher>(&self, state: &mut H) {
270✔
80
        FloatOrd(self.ratio).hash(state);
810✔
81
        FloatOrd(self.value).hash(state);
810✔
82
    }
83
}
84

85
impl Hash for GradientKey<Vec2> {
86
    fn hash<H: Hasher>(&self, state: &mut H) {
270✔
87
        FloatOrd(self.ratio).hash(state);
810✔
88
        FloatOrd(self.value.x).hash(state);
810✔
89
        FloatOrd(self.value.y).hash(state);
810✔
90
    }
91
}
92

93
impl Hash for GradientKey<Vec3> {
94
    fn hash<H: Hasher>(&self, state: &mut H) {
272✔
95
        FloatOrd(self.ratio).hash(state);
816✔
96
        FloatOrd(self.value.x).hash(state);
816✔
97
        FloatOrd(self.value.y).hash(state);
816✔
98
        FloatOrd(self.value.z).hash(state);
816✔
99
    }
100
}
101

102
impl Hash for GradientKey<Vec4> {
103
    fn hash<H: Hasher>(&self, state: &mut H) {
272✔
104
        FloatOrd(self.ratio).hash(state);
816✔
105
        FloatOrd(self.value.x).hash(state);
816✔
106
        FloatOrd(self.value.y).hash(state);
816✔
107
        FloatOrd(self.value.z).hash(state);
816✔
108
        FloatOrd(self.value.w).hash(state);
816✔
109
    }
110
}
111

112
/// A gradient curve made of keypoints and associated values.
113
///
114
/// The gradient can be sampled anywhere, and will return a linear interpolation
115
/// of the values of its closest keys. Sampling before 0 or after 1 returns a
116
/// constant value equal to the one of the closest bound.
117
///
118
/// # Construction
119
///
120
/// The most efficient constructors take the entirety of the key points upfront.
121
/// This prevents costly linear searches to insert key points one by one:
122
/// - [`constant()`] creates a gradient with a single key point;
123
/// - [`linear()`] creates a linear gradient between two key points;
124
/// - [`from_keys()`] creates a more general gradient with any number of key
125
///   points.
126
///
127
/// [`constant()`]: crate::Gradient::constant
128
/// [`linear()`]: crate::Gradient::linear
129
/// [`from_keys()`]: crate::Gradient::from_keys
130
#[derive(Debug, Default, Clone, PartialEq, Reflect, Serialize, Deserialize)]
131
pub struct Gradient<T: Lerp + FromReflect> {
132
    keys: Vec<GradientKey<T>>,
133
}
134

135
// SAFETY: This is consistent with the derive, but we can't derive due to trait
136
// bounds.
137
#[allow(clippy::derived_hash_with_manual_eq)]
138
impl<T> Hash for Gradient<T>
139
where
140
    T: Default + Lerp + FromReflect,
141
    GradientKey<T>: Hash,
142
{
143
    fn hash<H: Hasher>(&self, state: &mut H) {
230✔
144
        self.keys.hash(state);
690✔
145
    }
146
}
147

148
impl<T: Lerp + FromReflect> Gradient<T> {
149
    /// Create a new empty gradient.
150
    ///
151
    /// # Example
152
    ///
153
    /// ```
154
    /// # use bevy_hanabi::Gradient;
155
    /// let g: Gradient<f32> = Gradient::new();
156
    /// assert!(g.is_empty());
157
    /// ```
158
    pub const fn new() -> Self {
48✔
159
        Self { keys: vec![] }
48✔
160
    }
161

162
    /// Create a constant gradient.
163
    ///
164
    /// The gradient contains `value` at key 0.0 and nothing else. Any sampling
165
    /// evaluates to that single value.
166
    ///
167
    /// # Example
168
    ///
169
    /// ```
170
    /// # use bevy_hanabi::Gradient;
171
    /// # use bevy::math::Vec2;
172
    /// let g = Gradient::constant(Vec2::X);
173
    /// assert_eq!(g.sample(0.3), Vec2::X);
174
    /// ```
175
    pub fn constant(value: T) -> Self {
1✔
176
        Self {
177
            keys: vec![GradientKey::<T> { ratio: 0., value }],
1✔
178
        }
179
    }
180

181
    /// Create a linear gradient between two values.
182
    ///
183
    /// The gradient contains the `start` value at key 0.0 and the `end` value
184
    /// at key 1.0.
185
    ///
186
    /// # Example
187
    ///
188
    /// ```
189
    /// # use bevy_hanabi::Gradient;
190
    /// # use bevy::math::Vec3;
191
    /// let g = Gradient::linear(Vec3::ZERO, Vec3::Y);
192
    /// assert_eq!(g.sample(0.3), Vec3::new(0., 0.3, 0.));
193
    /// ```
194
    pub fn linear(start: T, end: T) -> Self {
×
195
        Self {
196
            keys: vec![
×
197
                GradientKey::<T> {
198
                    ratio: 0.,
199
                    value: start,
200
                },
201
                GradientKey::<T> {
202
                    ratio: 1.,
203
                    value: end,
204
                },
205
            ],
206
        }
207
    }
208

209
    /// Create a new gradient from a series of key points.
210
    ///
211
    /// If one or more duplicate ratios already exist, append each new key after
212
    /// all the existing keys with same ratio. The keys are inserted in order,
213
    /// but do not need to be sorted by ratio.
214
    ///
215
    /// The ratio must be a finite floating point value.
216
    ///
217
    /// This variant is slightly more performant than [`with_keys()`] because it
218
    /// can sort all keys before inserting them in batch.
219
    ///
220
    /// If you have only one or two keys, consider using [`constant()`] or
221
    /// [`linear()`], respectively, instead of this.
222
    ///
223
    /// # Example
224
    ///
225
    /// ```
226
    /// # use bevy_hanabi::Gradient;
227
    /// let g = Gradient::from_keys([(0., 3.2), (1., 13.89), (0.3, 9.33)]);
228
    /// assert_eq!(g.len(), 3);
229
    /// assert_eq!(g.sample(0.3), 9.33);
230
    /// ```
231
    ///
232
    /// # Panics
233
    ///
234
    /// This method panics if any `ratio` is not in the \[0:1\] range.
235
    ///
236
    /// [`with_keys()`]: crate::Gradient::with_keys
237
    /// [`constant()`]: crate::Gradient::constant
238
    /// [`linear()`]: crate::Gradient::linear
239
    pub fn from_keys(keys: impl IntoIterator<Item = (f32, T)>) -> Self {
1✔
240
        // Note that all operations below are stable, including the sort. This ensures
241
        // the keys are kept in the correct order.
242
        let mut keys = keys
2✔
243
            .into_iter()
244
            .map(|(ratio, value)| GradientKey { ratio, value })
7✔
245
            .collect::<Vec<_>>();
246
        keys.sort_by(|a, b| FloatOrd(a.ratio).cmp(&FloatOrd(b.ratio)));
5✔
247
        Self { keys }
248
    }
249

250
    /// Returns `true` if the gradient contains no key points.
251
    ///
252
    /// # Examples
253
    ///
254
    /// ```
255
    /// # use bevy_hanabi::Gradient;
256
    /// let mut g = Gradient::new();
257
    /// assert!(g.is_empty());
258
    ///
259
    /// g.add_key(0.3, 3.42);
260
    /// assert!(!g.is_empty());
261
    /// ```
262
    pub fn is_empty(&self) -> bool {
3✔
263
        self.keys.is_empty()
6✔
264
    }
265

266
    /// Returns the number of key points in the gradient, also referred to as
267
    /// its 'length'.
268
    ///
269
    /// # Examples
270
    ///
271
    /// ```
272
    /// # use bevy_hanabi::Gradient;
273
    /// let g = Gradient::linear(3.5, 7.8);
274
    /// assert_eq!(g.len(), 2);
275
    /// ```
276
    pub fn len(&self) -> usize {
7✔
277
        self.keys.len()
14✔
278
    }
279

280
    /// Add a key point to the gradient.
281
    ///
282
    /// If one or more duplicate ratios already exist, append the new key after
283
    /// all the existing keys with same ratio.
284
    ///
285
    /// The ratio must be a finite floating point value.
286
    ///
287
    /// Note that this function needs to perform a linear search into the
288
    /// gradient's key points to find an insertion point. If you already know
289
    /// all key points in advance, it's more efficient to use [`constant()`],
290
    /// [`linear()`], or [`with_keys()`].
291
    ///
292
    /// # Panics
293
    ///
294
    /// This method panics if `ratio` is not in the \[0:1\] range.
295
    ///
296
    /// [`constant()`]: crate::Gradient::constant
297
    /// [`linear()`]: crate::Gradient::linear
298
    /// [`with_keys()`]: crate::Gradient::with_keys
299
    pub fn with_key(mut self, ratio: f32, value: T) -> Self {
1✔
300
        self.add_key(ratio, value);
4✔
301
        self
1✔
302
    }
303

304
    /// Add a series of key points to the gradient.
305
    ///
306
    /// If one or more duplicate ratios already exist, append each new key after
307
    /// all the existing keys with same ratio. The keys are inserted in order.
308
    ///
309
    /// The ratio must be a finite floating point value.
310
    ///
311
    /// This variant is slightly more performant than [`add_key()`] because it
312
    /// can reserve storage for all key points upfront, which requires an exact
313
    /// size iterator.
314
    ///
315
    /// Note that if all key points are known upfront, [`from_keys()`] is a lot
316
    /// more performant.
317
    ///
318
    /// # Example
319
    ///
320
    /// ```
321
    /// # use bevy_hanabi::Gradient;
322
    /// fn add_some_keys(mut g: Gradient<f32>) -> Gradient<f32> {
323
    ///     g.with_keys([(0.7, 12.9), (0.32, 9.31)].into_iter())
324
    /// }
325
    /// ```
326
    ///
327
    /// # Panics
328
    ///
329
    /// This method panics if any `ratio` is not in the \[0:1\] range.
330
    ///
331
    /// [`add_key()`]: crate::Gradient::add_key
332
    /// [`from_keys()`]: crate::Gradient::from_keys
333
    pub fn with_keys(mut self, keys: impl ExactSizeIterator<Item = (f32, T)>) -> Self {
42✔
334
        self.keys.reserve(keys.len());
168✔
335
        for (ratio, value) in keys {
410✔
336
            self.add_key(ratio, value);
×
337
        }
338
        self
42✔
339
    }
340

341
    /// Add a key point to the gradient.
342
    ///
343
    /// If one or more duplicate ratios already exist, append the new key after
344
    /// all the existing keys with same ratio.
345
    ///
346
    /// The ratio must be a finite floating point value.
347
    ///
348
    /// # Panics
349
    ///
350
    /// This method panics if `ratio` is not in the \[0:1\] range.
351
    pub fn add_key(&mut self, ratio: f32, value: T) {
203✔
352
        assert!(ratio >= 0.0);
406✔
353
        assert!(ratio <= 1.0);
406✔
354
        let index = match self
406✔
355
            .keys
203✔
356
            .binary_search_by(|key| FloatOrd(key.ratio).cmp(&FloatOrd(ratio)))
1,415✔
357
        {
358
            Ok(mut index) => {
7✔
359
                // When there are duplicate keys, binary_search_by() returns the index of an
360
                // unspecified one. Make sure we insert always as the last
361
                // duplicate one, for determinism.
UNCOV
362
                let len = self.keys.len();
×
363
                while index + 1 < len && self.keys[index].ratio == self.keys[index + 1].ratio {
9✔
364
                    index += 1;
×
365
                }
366
                index + 1 // insert after last duplicate
×
367
            }
368
            Err(upper_index) => upper_index,
392✔
369
        };
370
        self.keys.insert(index, GradientKey { ratio, value });
812✔
371
    }
372

373
    /// Get the gradient keys.
374
    pub fn keys(&self) -> &[GradientKey<T>] {
70✔
375
        &self.keys[..]
70✔
376
    }
377

378
    /// Get mutable access to the gradient keys.
379
    pub fn keys_mut(&mut self) -> &mut [GradientKey<T>] {
72✔
380
        &mut self.keys[..]
72✔
381
    }
382

383
    /// Sample the gradient at the given ratio.
384
    ///
385
    /// If the ratio is exactly equal to those of one or more keys, sample the
386
    /// first key in the collection. If the ratio falls between two keys,
387
    /// return a linear interpolation of their values. If the ratio is
388
    /// before the first key or after the last one, return the first and
389
    /// last value, respectively.
390
    ///
391
    /// # Panics
392
    ///
393
    /// This method panics if the gradient is empty (has no key point).
394
    pub fn sample(&self, ratio: f32) -> T {
283✔
395
        assert!(!self.keys.is_empty());
566✔
396
        match self
283✔
397
            .keys
283✔
398
            .binary_search_by(|key| FloatOrd(key.ratio).cmp(&FloatOrd(ratio)))
1,966✔
399
        {
400
            Ok(mut index) => {
5✔
401
                // When there are duplicate keys, binary_search_by() returns the index of an
402
                // unspecified one. Make sure we sample the first duplicate, for determinism.
403
                while index > 0 && self.keys[index - 1].ratio == self.keys[index].ratio {
11✔
404
                    index -= 1;
1✔
405
                }
406
                self.keys[index].value
×
407
            }
408
            Err(upper_index) => {
278✔
409
                if upper_index > 0 {
278✔
410
                    if upper_index < self.keys.len() {
286✔
411
                        let key0 = &self.keys[upper_index - 1];
158✔
412
                        let key1 = &self.keys[upper_index];
158✔
413
                        let t = (ratio - key0.ratio) / (key1.ratio - key0.ratio);
237✔
414
                        key0.value.lerp(key1.value, t)
316✔
415
                    } else {
416
                        // post: sampling point located after the last key
417
                        self.keys[upper_index - 1].value
64✔
418
                    }
419
                } else {
420
                    // pre: sampling point located before the first key
421
                    self.keys[upper_index].value
135✔
422
                }
423
            }
424
        }
425
    }
426

427
    /// Sample the gradient at regular intervals.
428
    ///
429
    /// Create a list of sample points starting at ratio `start` and spaced with
430
    /// `inc` delta ratio. The number of samples is equal to the length of
431
    /// the `dst` slice. Sample the gradient at all those points, and fill
432
    /// the `dst` slice with the resulting values.
433
    ///
434
    /// This is equivalent to calling [`sample()`] in a loop, but is more
435
    /// efficient.
436
    ///
437
    /// [`sample()`]: Gradient::sample
438
    pub fn sample_by(&self, start: f32, inc: f32, dst: &mut [T]) {
1✔
439
        let count = dst.len();
3✔
440
        assert!(!self.keys.is_empty());
2✔
441
        let mut ratio = start;
2✔
442
        // pre: sampling points located before the first key
443
        let first_ratio = self.keys[0].ratio;
2✔
444
        let first_col = self.keys[0].value;
2✔
445
        let mut idst = 0;
2✔
446
        while idst < count && ratio <= first_ratio {
389✔
447
            dst[idst] = first_col;
129✔
448
            idst += 1;
129✔
449
            ratio += inc;
129✔
450
        }
451
        // main: sampling points located on or after the first key
452
        let mut ikey = 1;
2✔
453
        let len = self.keys.len();
3✔
454
        for i in idst..count {
79✔
455
            // Find the first key after the ratio
456
            while ikey < len && ratio > self.keys[ikey].ratio {
233✔
457
                ikey += 1;
1✔
458
            }
459
            if ikey >= len {
77✔
460
                // post: sampling points located after the last key
461
                let last_col = self.keys[len - 1].value;
1✔
462
                for d in &mut dst[i..] {
51✔
463
                    *d = last_col;
×
464
                }
465
                return;
×
466
            }
UNCOV
467
            if self.keys[ikey].ratio == ratio {
×
468
                dst[i] = self.keys[ikey].value;
×
469
            } else {
470
                let k0 = &self.keys[ikey - 1];
76✔
471
                let k1 = &self.keys[ikey];
76✔
472
                let t = (ratio - k0.ratio) / (k1.ratio - k0.ratio);
76✔
473
                dst[i] = k0.value.lerp(k1.value, t);
76✔
474
            }
475
            ratio += inc;
×
476
        }
477
    }
478
}
479

480
#[cfg(test)]
481
mod tests {
482
    use std::collections::hash_map::DefaultHasher;
483

484
    use bevy::reflect::{PartialReflect, ReflectRef, Struct};
485
    use rand::{distributions::Standard, prelude::Distribution, rngs::ThreadRng, thread_rng, Rng};
486

487
    use super::*;
488
    use crate::test_utils::*;
489

490
    const RED: Vec4 = Vec4::new(1., 0., 0., 1.);
491
    const BLUE: Vec4 = Vec4::new(0., 0., 1., 1.);
492
    const GREEN: Vec4 = Vec4::new(0., 1., 0., 1.);
493

494
    fn make_test_gradient() -> Gradient<Vec4> {
495
        let mut g = Gradient::new();
496
        g.add_key(0.5, RED);
497
        g.add_key(0.8, BLUE);
498
        g.add_key(0.8, GREEN);
499
        g
500
    }
501

502
    fn color_approx_eq(c0: Vec4, c1: Vec4, tol: f32) -> bool {
503
        ((c0.x - c1.x).abs() < tol)
504
            && ((c0.y - c1.y).abs() < tol)
505
            && ((c0.z - c1.z).abs() < tol)
506
            && ((c0.w - c1.w).abs() < tol)
507
    }
508

509
    #[test]
510
    fn lerp_test() {
511
        assert_approx_eq!(Lerp::lerp(3_f32, 5_f32, 0.1), 3.2_f32);
512
        assert_approx_eq!(Lerp::lerp(3_f32, 5_f32, 0.5), 4.0_f32);
513
        assert_approx_eq!(Lerp::lerp(3_f32, 5_f32, 0.9), 4.8_f32);
514
        assert_approx_eq!(Lerp::lerp(5_f32, 3_f32, 0.1), 4.8_f32);
515
        assert_approx_eq!(Lerp::lerp(5_f32, 3_f32, 0.5), 4.0_f32);
516
        assert_approx_eq!(Lerp::lerp(5_f32, 3_f32, 0.9), 3.2_f32);
517

518
        assert_approx_eq!(Lerp::lerp(3_f64, 5_f64, 0.1), 3.2_f64);
519
        assert_approx_eq!(Lerp::lerp(3_f64, 5_f64, 0.5), 4.0_f64);
520
        assert_approx_eq!(Lerp::lerp(3_f64, 5_f64, 0.9), 4.8_f64);
521
        assert_approx_eq!(Lerp::lerp(5_f64, 3_f64, 0.1), 4.8_f64);
522
        assert_approx_eq!(Lerp::lerp(5_f64, 3_f64, 0.5), 4.0_f64);
523
        assert_approx_eq!(Lerp::lerp(5_f64, 3_f64, 0.9), 3.2_f64);
524

525
        let s = Quat::IDENTITY;
526
        let e = Quat::from_rotation_x(90_f32.to_radians());
527
        assert_approx_eq!(Lerp::lerp(s, e, 0.1), s.slerp(e, 0.1));
528
        assert_approx_eq!(Lerp::lerp(s, e, 0.5), s.slerp(e, 0.5));
529
        assert_approx_eq!(Lerp::lerp(s, e, 0.9), s.slerp(e, 0.9));
530
        assert_approx_eq!(Lerp::lerp(e, s, 0.1), s.slerp(e, 0.9));
531
        assert_approx_eq!(Lerp::lerp(e, s, 0.5), s.slerp(e, 0.5));
532
        assert_approx_eq!(Lerp::lerp(e, s, 0.9), s.slerp(e, 0.1));
533
    }
534

535
    #[test]
536
    fn constant() {
537
        let grad = Gradient::constant(3.0);
538
        assert!(!grad.is_empty());
539
        assert_eq!(grad.len(), 1);
540
        for r in [
541
            -1e5, -0.5, -0.0001, 0., 0.0001, 0.3, 0.5, 0.9, 0.9999, 1., 1.0001, 100., 1e5,
542
        ] {
543
            assert_approx_eq!(grad.sample(r), 3.0);
544
        }
545
    }
546

547
    #[test]
548
    fn with_keys() {
549
        let g = Gradient::new().with_keys([(0.5, RED), (0.8, BLUE)].into_iter());
550
        assert_eq!(g.len(), 2);
551
        // Keys are inserted in order and after existing ones -> (R, B, B, R)
552
        let g2 = g.with_keys([(0.5, BLUE), (0.8, RED)].into_iter());
553
        assert_eq!(g2.len(), 4);
554
        assert_eq!(g2.sample(0.499), RED);
555
        assert_eq!(g2.sample(0.501), BLUE);
556
        assert_eq!(g2.sample(0.799), BLUE);
557
        assert_eq!(g2.sample(0.801), RED);
558
    }
559

560
    #[test]
561
    fn add_key() {
562
        let mut g = Gradient::new();
563
        assert!(g.is_empty());
564
        assert_eq!(g.len(), 0);
565
        g.add_key(0.3, RED);
566
        assert!(!g.is_empty());
567
        assert_eq!(g.len(), 1);
568
        // duplicate keys allowed
569
        let mut g = g.with_key(0.3, RED);
570
        assert_eq!(g.len(), 2);
571
        // duplicate ratios stored in order they're inserted
572
        g.add_key(0.7, BLUE);
573
        g.add_key(0.7, GREEN);
574
        assert_eq!(g.len(), 4);
575
        let keys = g.keys();
576
        assert_eq!(keys.len(), 4);
577
        assert!(color_approx_eq(RED, keys[0].value, 1e-5));
578
        assert!(color_approx_eq(RED, keys[1].value, 1e-5));
579
        assert!(color_approx_eq(BLUE, keys[2].value, 1e-5));
580
        assert!(color_approx_eq(GREEN, keys[3].value, 1e-5));
581
    }
582

583
    #[test]
584
    fn sample() {
585
        let mut g = Gradient::new();
586
        g.add_key(0.5, RED);
587
        assert_eq!(RED, g.sample(0.0));
588
        assert_eq!(RED, g.sample(0.5));
589
        assert_eq!(RED, g.sample(1.0));
590
        g.add_key(0.8, BLUE);
591
        g.add_key(0.8, GREEN);
592
        assert_eq!(RED, g.sample(0.0));
593
        assert_eq!(RED, g.sample(0.499));
594
        assert_eq!(RED, g.sample(0.5));
595
        let expected = RED.lerp(BLUE, 1. / 3.);
596
        let actual = g.sample(0.6);
597
        assert!(color_approx_eq(actual, expected, 1e-5));
598
        assert_eq!(BLUE, g.sample(0.8));
599
        assert_eq!(GREEN, g.sample(0.801));
600
        assert_eq!(GREEN, g.sample(1.0));
601
    }
602

603
    #[test]
604
    fn sample_by() {
605
        let g = Gradient::from_keys([(0.5, RED), (0.8, BLUE)]);
606
        const COUNT: usize = 256;
607
        let mut data: [Vec4; COUNT] = [Vec4::ZERO; COUNT];
608
        let start = 0.;
609
        let inc = 1. / COUNT as f32;
610
        g.sample_by(start, inc, &mut data[..]);
611
        for (i, &d) in data.iter().enumerate() {
612
            let ratio = inc.mul_add(i as f32, start);
613
            let expected = g.sample(ratio);
614
            assert!(color_approx_eq(expected, d, 1e-5));
615
        }
616
    }
617

618
    #[test]
619
    fn reflect() {
620
        let g = make_test_gradient();
621

622
        // Reflect
623
        let reflect: &dyn PartialReflect = &g;
624
        assert!(reflect
625
            .get_represented_type_info()
626
            .unwrap()
627
            .is::<Gradient<Vec4>>());
628
        let g_reflect = reflect.try_downcast_ref::<Gradient<Vec4>>();
629
        assert!(g_reflect.is_some());
630
        let g_reflect = g_reflect.unwrap();
631
        assert_eq!(*g_reflect, g);
632

633
        // FromReflect
634
        let g_from = Gradient::<Vec4>::from_reflect(reflect).unwrap();
635
        assert_eq!(g_from, g);
636

637
        // Struct
638
        assert!(g
639
            .get_represented_type_info()
640
            .unwrap()
641
            .type_path()
642
            .starts_with("bevy_hanabi::gradient::Gradient<")); // the Vec4 type name depends on platform
643
        let keys = g.field("keys").unwrap();
644
        let ReflectRef::List(keys) = keys.reflect_ref() else {
645
            panic!("Invalid type");
646
        };
647
        assert_eq!(keys.len(), 3);
648
        for (i, (r, v)) in [(0.5, RED), (0.8, BLUE), (0.8, GREEN)].iter().enumerate() {
649
            let k = keys.get(i).unwrap();
650
            let gk = k.try_downcast_ref::<GradientKey<Vec4>>().unwrap();
651
            assert_approx_eq!(gk.ratio(), r);
652
            assert_approx_eq!(gk.value, v);
653

654
            let ReflectRef::Struct(k) = k.reflect_ref() else {
655
                panic!("Invalid type");
656
            };
657
            assert!(k
658
                .get_represented_type_info()
659
                .unwrap()
660
                .type_path()
661
                .contains("GradientKey"));
662
        }
663
    }
664

665
    #[test]
666
    fn serde() {
667
        let g = make_test_gradient();
668

669
        let s = ron::to_string(&g).unwrap();
670
        // println!("gradient: {:?}", s);
671
        let g_serde: Gradient<Vec4> = ron::from_str(&s).unwrap();
672
        assert_eq!(g, g_serde);
673
    }
674

675
    /// Hash the given gradient.
676
    fn hash_gradient<T>(g: &Gradient<T>) -> u64
677
    where
678
        T: Default + Lerp + FromReflect,
679
        GradientKey<T>: Hash,
680
    {
681
        let mut hasher = DefaultHasher::default();
682
        g.hash(&mut hasher);
683
        hasher.finish()
684
    }
685

686
    /// Make a collection of random keys, for testing.
687
    fn make_keys<R, T, S>(rng: &mut R, count: usize) -> Vec<(f32, T)>
688
    where
689
        R: Rng + ?Sized,
690
        T: Lerp + FromReflect + From<S>,
691
        Standard: Distribution<S>,
692
    {
693
        if count == 0 {
694
            return vec![];
695
        }
696
        if count == 1 {
697
            return vec![(0., rng.gen().into())];
698
        }
699
        let mut ret = Vec::with_capacity(count);
700
        for i in 0..count {
701
            ret.push((i as f32 / (count - 1) as f32, rng.gen().into()));
702
        }
703
        ret
704
    }
705

706
    #[test]
707
    fn hash() {
708
        let mut rng = thread_rng();
709
        for count in 0..10 {
710
            let keys: Vec<(f32, f32)> = make_keys::<ThreadRng, f32, f32>(&mut rng, count);
711
            let mut g1 = Gradient::new().with_keys(keys.into_iter());
712
            let g2 = g1.clone();
713
            assert_eq!(g1, g2);
714
            assert_eq!(hash_gradient(&g1), hash_gradient(&g2));
715
            if count > 0 {
716
                g1.keys_mut()[0].value += 1.;
717
                assert_ne!(g1, g2);
718
                assert_ne!(hash_gradient(&g1), hash_gradient(&g2));
719
                g1.keys_mut()[0].value = g2.keys()[0].value;
720
                assert_eq!(g1, g2);
721
                assert_eq!(hash_gradient(&g1), hash_gradient(&g2));
722
            }
723
        }
724

725
        let mut rng = thread_rng();
726
        for count in 0..10 {
727
            let keys: Vec<(f32, Vec2)> = make_keys::<ThreadRng, Vec2, (f32, f32)>(&mut rng, count);
728
            let mut g1 = Gradient::new().with_keys(keys.into_iter());
729
            let g2 = g1.clone();
730
            assert_eq!(g1, g2);
731
            assert_eq!(hash_gradient(&g1), hash_gradient(&g2));
732
            if count > 0 {
733
                g1.keys_mut()[0].value += 1.;
734
                assert_ne!(g1, g2);
735
                assert_ne!(hash_gradient(&g1), hash_gradient(&g2));
736
                g1.keys_mut()[0].value = g2.keys()[0].value;
737
                assert_eq!(g1, g2);
738
                assert_eq!(hash_gradient(&g1), hash_gradient(&g2));
739
            }
740
        }
741

742
        let mut rng = thread_rng();
743
        for count in 0..10 {
744
            let keys: Vec<(f32, Vec3)> =
745
                make_keys::<ThreadRng, Vec3, (f32, f32, f32)>(&mut rng, count);
746
            let mut g1 = Gradient::new().with_keys(keys.into_iter());
747
            let g2 = g1.clone();
748
            assert_eq!(g1, g2);
749
            assert_eq!(hash_gradient(&g1), hash_gradient(&g2));
750
            if count > 0 {
751
                g1.keys_mut()[0].value += 1.;
752
                assert_ne!(g1, g2);
753
                assert_ne!(hash_gradient(&g1), hash_gradient(&g2));
754
                g1.keys_mut()[0].value = g2.keys()[0].value;
755
                assert_eq!(g1, g2);
756
                assert_eq!(hash_gradient(&g1), hash_gradient(&g2));
757
            }
758
        }
759

760
        let mut rng = thread_rng();
761
        for count in 0..10 {
762
            let keys: Vec<(f32, Vec4)> =
763
                make_keys::<ThreadRng, Vec4, (f32, f32, f32, f32)>(&mut rng, count);
764
            let mut g1 = Gradient::new().with_keys(keys.into_iter());
765
            let g2 = g1.clone();
766
            assert_eq!(g1, g2);
767
            assert_eq!(hash_gradient(&g1), hash_gradient(&g2));
768
            if count > 0 {
769
                g1.keys_mut()[0].value += 1.;
770
                assert_ne!(g1, g2);
771
                assert_ne!(hash_gradient(&g1), hash_gradient(&g2));
772
                g1.keys_mut()[0].value = g2.keys()[0].value;
773
                assert_eq!(g1, g2);
774
                assert_eq!(hash_gradient(&g1), hash_gradient(&g2));
775
            }
776
        }
777
    }
778
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc