token_value_map/
value.rs

1use crate::*;
2use core::num::NonZeroU16;
3use std::hash::{Hash, Hasher};
4
5/// Type alias for bracket sampling return type.
6type BracketSample = (Option<(Time, Data)>, Option<(Time, Data)>);
7
8/// A value that can be either uniform or animated over time.
9///
10/// A [`Value`] contains either a single [`Data`] value that remains constant
11/// (uniform) or [`AnimatedData`] that changes over time with interpolation.
12#[derive(Clone, Debug, PartialEq, Hash)]
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14#[cfg_attr(feature = "facet", derive(Facet))]
15#[cfg_attr(feature = "facet", facet(opaque))]
16#[cfg_attr(feature = "facet", repr(u8))]
17pub enum Value {
18    /// A constant value that does not change over time.
19    Uniform(Data),
20    /// A value that changes over time with keyframe interpolation.
21    Animated(AnimatedData),
22}
23
24impl Value {
25    /// Create a uniform value that does not change over time.
26    pub fn uniform<V: Into<Data>>(value: V) -> Self {
27        Value::Uniform(value.into())
28    }
29
30    /// Create an animated value from time-value pairs.
31    ///
32    /// All samples must have the same data type. Vector samples are padded
33    /// to match the length of the longest vector in the set.
34    pub fn animated<I, V>(samples: I) -> Result<Self>
35    where
36        I: IntoIterator<Item = (Time, V)>,
37        V: Into<Data>,
38    {
39        let mut samples_vec: Vec<(Time, Data)> =
40            samples.into_iter().map(|(t, v)| (t, v.into())).collect();
41
42        if samples_vec.is_empty() {
43            return Err(anyhow!("Cannot create animated value with no samples"));
44        }
45
46        // Get the data type from the first sample
47        let data_type = samples_vec[0].1.data_type();
48
49        // Check all samples have the same type and handle length consistency
50        let mut expected_len: Option<usize> = None;
51        for (time, value) in &mut samples_vec {
52            if value.data_type() != data_type {
53                return Err(anyhow!(
54                    "All animated samples must have the same type. Expected {:?}, found {:?} at time {}",
55                    data_type,
56                    value.data_type(),
57                    time
58                ));
59            }
60
61            // Check vector length consistency
62            if let Some(vec_len) = value.try_len() {
63                match expected_len {
64                    None => expected_len = Some(vec_len),
65                    Some(expected) => {
66                        if vec_len > expected {
67                            return Err(anyhow!(
68                                "Vector length {} exceeds expected length {} at time {}",
69                                vec_len,
70                                expected,
71                                time
72                            ));
73                        } else if vec_len < expected {
74                            // Pad to expected length
75                            value.pad_to_length(expected);
76                        }
77                    }
78                }
79            }
80        }
81
82        // Create the appropriate AnimatedData variant by extracting the
83        // specific data type
84
85        let animated_data = match data_type {
86            DataType::Boolean => {
87                let typed_samples: Vec<(Time, Boolean)> = samples_vec
88                    .into_iter()
89                    .map(|(t, data)| match data {
90                        Data::Boolean(v) => (t, v),
91                        _ => unreachable!("Type validation should have caught this"),
92                    })
93                    .collect();
94                AnimatedData::Boolean(TimeDataMap::from_iter(typed_samples))
95            }
96            DataType::Integer => {
97                let typed_samples: Vec<(Time, Integer)> = samples_vec
98                    .into_iter()
99                    .map(|(t, data)| match data {
100                        Data::Integer(v) => (t, v),
101                        _ => unreachable!("Type validation should have caught this"),
102                    })
103                    .collect();
104                AnimatedData::Integer(TimeDataMap::from_iter(typed_samples))
105            }
106            DataType::Real => {
107                let typed_samples: Vec<(Time, Real)> = samples_vec
108                    .into_iter()
109                    .map(|(t, data)| match data {
110                        Data::Real(v) => (t, v),
111                        _ => unreachable!("Type validation should have caught this"),
112                    })
113                    .collect();
114                AnimatedData::Real(TimeDataMap::from_iter(typed_samples))
115            }
116            DataType::String => {
117                let typed_samples: Vec<(Time, String)> = samples_vec
118                    .into_iter()
119                    .map(|(t, data)| match data {
120                        Data::String(v) => (t, v),
121                        _ => unreachable!("Type validation should have caught this"),
122                    })
123                    .collect();
124                AnimatedData::String(TimeDataMap::from_iter(typed_samples))
125            }
126            DataType::Color => {
127                let typed_samples: Vec<(Time, Color)> = samples_vec
128                    .into_iter()
129                    .map(|(t, data)| match data {
130                        Data::Color(v) => (t, v),
131                        _ => unreachable!("Type validation should have caught this"),
132                    })
133                    .collect();
134                AnimatedData::Color(TimeDataMap::from_iter(typed_samples))
135            }
136            #[cfg(feature = "vector2")]
137            DataType::Vector2 => {
138                let typed_samples: Vec<(Time, Vector2)> = samples_vec
139                    .into_iter()
140                    .map(|(t, data)| match data {
141                        Data::Vector2(v) => (t, v),
142                        _ => unreachable!("Type validation should have caught this"),
143                    })
144                    .collect();
145                AnimatedData::Vector2(TimeDataMap::from_iter(typed_samples))
146            }
147            #[cfg(feature = "vector3")]
148            DataType::Vector3 => {
149                let typed_samples: Vec<(Time, Vector3)> = samples_vec
150                    .into_iter()
151                    .map(|(t, data)| match data {
152                        Data::Vector3(v) => (t, v),
153                        _ => unreachable!("Type validation should have caught this"),
154                    })
155                    .collect();
156                AnimatedData::Vector3(TimeDataMap::from_iter(typed_samples))
157            }
158            #[cfg(feature = "matrix3")]
159            DataType::Matrix3 => {
160                let typed_samples: Vec<(Time, Matrix3)> = samples_vec
161                    .into_iter()
162                    .map(|(t, data)| match data {
163                        Data::Matrix3(v) => (t, v),
164                        _ => unreachable!("Type validation should have caught this"),
165                    })
166                    .collect();
167                AnimatedData::Matrix3(TimeDataMap::from_iter(typed_samples))
168            }
169            #[cfg(feature = "normal3")]
170            DataType::Normal3 => {
171                let typed_samples: Vec<(Time, Normal3)> = samples_vec
172                    .into_iter()
173                    .map(|(t, data)| match data {
174                        Data::Normal3(v) => (t, v),
175                        _ => unreachable!("Type validation should have caught this"),
176                    })
177                    .collect();
178                AnimatedData::Normal3(TimeDataMap::from_iter(typed_samples))
179            }
180            #[cfg(feature = "point3")]
181            DataType::Point3 => {
182                let typed_samples: Vec<(Time, Point3)> = samples_vec
183                    .into_iter()
184                    .map(|(t, data)| match data {
185                        Data::Point3(v) => (t, v),
186                        _ => unreachable!("Type validation should have caught this"),
187                    })
188                    .collect();
189                AnimatedData::Point3(TimeDataMap::from_iter(typed_samples))
190            }
191            #[cfg(feature = "matrix4")]
192            DataType::Matrix4 => {
193                let typed_samples: Vec<(Time, Matrix4)> = samples_vec
194                    .into_iter()
195                    .map(|(t, data)| match data {
196                        Data::Matrix4(v) => (t, v),
197                        _ => unreachable!("Type validation should have caught this"),
198                    })
199                    .collect();
200                AnimatedData::Matrix4(TimeDataMap::from_iter(typed_samples))
201            }
202            DataType::BooleanVec => {
203                let typed_samples: Vec<(Time, BooleanVec)> = samples_vec
204                    .into_iter()
205                    .map(|(t, data)| match data {
206                        Data::BooleanVec(v) => (t, v),
207                        _ => unreachable!("Type validation should have caught this"),
208                    })
209                    .collect();
210                AnimatedData::BooleanVec(TimeDataMap::from_iter(typed_samples))
211            }
212            DataType::IntegerVec => {
213                let typed_samples: Vec<(Time, IntegerVec)> = samples_vec
214                    .into_iter()
215                    .map(|(t, data)| match data {
216                        Data::IntegerVec(v) => (t, v),
217                        _ => unreachable!("Type validation should have caught this"),
218                    })
219                    .collect();
220                AnimatedData::IntegerVec(TimeDataMap::from_iter(typed_samples))
221            }
222            DataType::RealVec => {
223                let typed_samples: Vec<(Time, RealVec)> = samples_vec
224                    .into_iter()
225                    .map(|(t, data)| match data {
226                        Data::RealVec(v) => (t, v),
227                        _ => unreachable!("Type validation should have caught this"),
228                    })
229                    .collect();
230                AnimatedData::RealVec(TimeDataMap::from_iter(typed_samples))
231            }
232            DataType::ColorVec => {
233                let typed_samples: Vec<(Time, ColorVec)> = samples_vec
234                    .into_iter()
235                    .map(|(t, data)| match data {
236                        Data::ColorVec(v) => (t, v),
237                        _ => unreachable!("Type validation should have caught this"),
238                    })
239                    .collect();
240                AnimatedData::ColorVec(TimeDataMap::from_iter(typed_samples))
241            }
242            DataType::StringVec => {
243                let typed_samples: Vec<(Time, StringVec)> = samples_vec
244                    .into_iter()
245                    .map(|(t, data)| match data {
246                        Data::StringVec(v) => (t, v),
247                        _ => unreachable!("Type validation should have caught this"),
248                    })
249                    .collect();
250                AnimatedData::StringVec(TimeDataMap::from_iter(typed_samples))
251            }
252            #[cfg(all(feature = "vector2", feature = "vec_variants"))]
253            DataType::Vector2Vec => {
254                let typed_samples: Vec<(Time, Vector2Vec)> = samples_vec
255                    .into_iter()
256                    .map(|(t, data)| match data {
257                        Data::Vector2Vec(v) => (t, v),
258                        _ => unreachable!("Type validation should have caught this"),
259                    })
260                    .collect();
261                AnimatedData::Vector2Vec(TimeDataMap::from_iter(typed_samples))
262            }
263            #[cfg(all(feature = "vector3", feature = "vec_variants"))]
264            DataType::Vector3Vec => {
265                let typed_samples: Vec<(Time, Vector3Vec)> = samples_vec
266                    .into_iter()
267                    .map(|(t, data)| match data {
268                        Data::Vector3Vec(v) => (t, v),
269                        _ => unreachable!("Type validation should have caught this"),
270                    })
271                    .collect();
272                AnimatedData::Vector3Vec(TimeDataMap::from_iter(typed_samples))
273            }
274            #[cfg(all(feature = "matrix3", feature = "vec_variants"))]
275            DataType::Matrix3Vec => {
276                let typed_samples: Vec<(Time, Matrix3Vec)> = samples_vec
277                    .into_iter()
278                    .map(|(t, data)| match data {
279                        Data::Matrix3Vec(v) => (t, v),
280                        _ => unreachable!("Type validation should have caught this"),
281                    })
282                    .collect();
283                AnimatedData::Matrix3Vec(TimeDataMap::from_iter(typed_samples))
284            }
285            #[cfg(all(feature = "normal3", feature = "vec_variants"))]
286            DataType::Normal3Vec => {
287                let typed_samples: Vec<(Time, Normal3Vec)> = samples_vec
288                    .into_iter()
289                    .map(|(t, data)| match data {
290                        Data::Normal3Vec(v) => (t, v),
291                        _ => unreachable!("Type validation should have caught this"),
292                    })
293                    .collect();
294                AnimatedData::Normal3Vec(TimeDataMap::from_iter(typed_samples))
295            }
296            #[cfg(all(feature = "point3", feature = "vec_variants"))]
297            DataType::Point3Vec => {
298                let typed_samples: Vec<(Time, Point3Vec)> = samples_vec
299                    .into_iter()
300                    .map(|(t, data)| match data {
301                        Data::Point3Vec(v) => (t, v),
302                        _ => unreachable!("Type validation should have caught this"),
303                    })
304                    .collect();
305                AnimatedData::Point3Vec(TimeDataMap::from_iter(typed_samples))
306            }
307            #[cfg(all(feature = "matrix4", feature = "vec_variants"))]
308            DataType::Matrix4Vec => {
309                let typed_samples: Vec<(Time, Matrix4Vec)> = samples_vec
310                    .into_iter()
311                    .map(|(t, data)| match data {
312                        Data::Matrix4Vec(v) => (t, v),
313                        _ => unreachable!("Type validation should have caught this"),
314                    })
315                    .collect();
316                AnimatedData::Matrix4Vec(TimeDataMap::from_iter(typed_samples))
317            }
318        };
319
320        Ok(Value::Animated(animated_data))
321    }
322
323    /// Add a sample at a specific time, checking length constraints
324    pub fn add_sample<V: Into<Data>>(&mut self, time: Time, val: V) -> Result<()> {
325        let value = val.into();
326
327        match self {
328            Value::Uniform(_uniform_value) => {
329                // Switch to animated and drop/ignore the existing uniform
330                // content Create a new animated value with only
331                // the new sample
332                *self = Value::animated(vec![(time, value)])?;
333                Ok(())
334            }
335            Value::Animated(samples) => {
336                let data_type = samples.data_type();
337                if value.data_type() != data_type {
338                    return Err(anyhow!(
339                        "Type mismatch: cannot add {:?} to animated {:?}",
340                        value.data_type(),
341                        data_type
342                    ));
343                }
344
345                // Insert the value using the generic insert method
346                samples.try_insert(time, value)
347            }
348        }
349    }
350
351    /// Remove a sample at a specific time.
352    ///
353    /// Returns the removed value if it existed. For uniform values, this is a
354    /// no-op and returns `None`. If the last sample is removed from an
355    /// animated value, the value remains animated but empty.
356    pub fn remove_sample(&mut self, time: &Time) -> Option<Data> {
357        match self {
358            Value::Uniform(_) => None,
359            Value::Animated(samples) => samples.remove_at(time),
360        }
361    }
362
363    /// Sample value at exact time without interpolation.
364    ///
365    /// Returns the exact value if it exists at the given time, or `None` if
366    /// no sample exists at that time for animated values.
367    pub fn sample_at(&self, time: Time) -> Option<Data> {
368        match self {
369            Value::Uniform(v) => Some(v.clone()),
370            Value::Animated(samples) => samples.sample_at(time),
371        }
372    }
373
374    /// Get the value at or before the given time
375    pub fn sample_at_or_before(&self, time: Time) -> Option<Data> {
376        match self {
377            Value::Uniform(v) => Some(v.clone()),
378            Value::Animated(_samples) => {
379                // For now, use interpolation at the exact time
380                // TODO: Implement proper at-or-before sampling in AnimatedData
381                Some(self.interpolate(time))
382            }
383        }
384    }
385
386    /// Get the value at or after the given time
387    pub fn sample_at_or_after(&self, time: Time) -> Option<Data> {
388        match self {
389            Value::Uniform(v) => Some(v.clone()),
390            Value::Animated(_samples) => {
391                // For now, use interpolation at the exact time
392                // TODO: Implement proper at-or-after sampling in AnimatedData
393                Some(self.interpolate(time))
394            }
395        }
396    }
397
398    /// Interpolate value at the given time.
399    ///
400    /// For uniform values, returns the constant value. For animated values,
401    /// interpolates between surrounding keyframes using appropriate
402    /// interpolation methods (linear, quadratic, or hermite).
403    pub fn interpolate(&self, time: Time) -> Data {
404        match self {
405            Value::Uniform(v) => v.clone(),
406            Value::Animated(samples) => samples.interpolate(time),
407        }
408    }
409
410    /// Get surrounding samples for interpolation.
411    pub fn sample_surrounding<const N: usize>(&self, time: Time) -> SmallVec<[(Time, Data); N]> {
412        let mut result = SmallVec::<[(Time, Data); N]>::new_const();
413        match self {
414            Value::Uniform(v) => result.push((time, v.clone())),
415            Value::Animated(_samples) => {
416                // TODO: Implement proper surrounding sample collection for
417                // AnimatedData For now, just return the
418                // interpolated value at the given time
419                let value = self.interpolate(time);
420                result.push((time, value));
421            }
422        }
423        result
424    }
425
426    /// Get the two samples surrounding a time for linear interpolation
427    pub fn sample_bracket(&self, time: Time) -> BracketSample {
428        match self {
429            Value::Uniform(v) => (Some((time, v.clone())), None),
430            Value::Animated(_samples) => {
431                // TODO: Implement proper bracketing for AnimatedData
432                // For now, just return the interpolated value at the given time
433                let value = self.interpolate(time);
434                (Some((time, value)), None)
435            }
436        }
437    }
438
439    /// Check if the value is animated.
440    pub fn is_animated(&self) -> bool {
441        match self {
442            Value::Uniform(_) => false,
443            Value::Animated(samples) => samples.is_animated(),
444        }
445    }
446
447    /// Get the number of time samples.
448    pub fn sample_count(&self) -> usize {
449        match self {
450            Value::Uniform(_) => 1,
451            Value::Animated(samples) => samples.len(),
452        }
453    }
454
455    /// Get all time samples.
456    pub fn times(&self) -> SmallVec<[Time; 10]> {
457        match self {
458            Value::Uniform(_) => SmallVec::<[Time; 10]>::new_const(),
459            Value::Animated(samples) => samples.times(),
460        }
461    }
462
463    /// Merge this value with another using a combiner function.
464    ///
465    /// For uniform values, applies the combiner once.
466    /// For animated values, samples both at the union of all keyframe times
467    /// and applies the combiner at each time.
468    ///
469    /// # Example
470    /// ```ignore
471    /// // Multiply two matrices
472    /// let result = matrix1.merge_with(&matrix2, |a, b| {
473    ///     match (a, b) {
474    ///         (Data::Matrix3(m1), Data::Matrix3(m2)) => {
475    ///             Data::Matrix3(Matrix3(m1.0 * m2.0))
476    ///         }
477    ///         _ => a, // fallback
478    ///     }
479    /// })?;
480    /// ```
481    pub fn merge_with<F>(&self, other: &Value, combiner: F) -> Result<Value>
482    where
483        F: Fn(&Data, &Data) -> Data,
484    {
485        match (self, other) {
486            // Both uniform: simple case
487            (Value::Uniform(a), Value::Uniform(b)) => Ok(Value::Uniform(combiner(a, b))),
488
489            // One or both animated: need to sample at union of times
490            _ => {
491                // Collect all unique times from both values
492                let mut all_times = std::collections::BTreeSet::new();
493
494                // Add times from self
495                for t in self.times() {
496                    all_times.insert(t);
497                }
498
499                // Add times from other
500                for t in other.times() {
501                    all_times.insert(t);
502                }
503
504                // If no times found (both were uniform with no times), sample at default
505                if all_times.is_empty() {
506                    let a = self.interpolate(Time::default());
507                    let b = other.interpolate(Time::default());
508                    return Ok(Value::Uniform(combiner(&a, &b)));
509                }
510
511                // Sample both values at all times and combine
512                let mut combined_samples = Vec::new();
513                for time in all_times {
514                    let a = self.interpolate(time);
515                    let b = other.interpolate(time);
516                    let combined = combiner(&a, &b);
517                    combined_samples.push((time, combined));
518                }
519
520                // If only one sample, return as uniform
521                if combined_samples.len() == 1 {
522                    Ok(Value::Uniform(combined_samples[0].1.clone()))
523                } else {
524                    // Create animated value from combined samples
525                    Value::animated(combined_samples)
526                }
527            }
528        }
529    }
530}
531
532// From implementations for Value
533impl<V: Into<Data>> From<V> for Value {
534    fn from(value: V) -> Self {
535        Value::uniform(value)
536    }
537}
538
539// Sample trait implementations for Value using macro
540#[cfg(feature = "vector2")]
541impl_sample_for_value!(Vector2, Vector2);
542#[cfg(feature = "vector3")]
543impl_sample_for_value!(Vector3, Vector3);
544impl_sample_for_value!(Color, Color);
545#[cfg(feature = "matrix3")]
546impl_sample_for_value!(Matrix3, Matrix3);
547#[cfg(feature = "normal3")]
548impl_sample_for_value!(Normal3, Normal3);
549#[cfg(feature = "point3")]
550impl_sample_for_value!(Point3, Point3);
551#[cfg(feature = "matrix4")]
552impl_sample_for_value!(Matrix4, Matrix4);
553
554// Special implementations for Real and Integer that handle type conversion
555impl Sample<Real> for Value {
556    fn sample(&self, shutter: &Shutter, samples: NonZeroU16) -> Result<Vec<(Real, SampleWeight)>> {
557        match self {
558            Value::Uniform(data) => {
559                let value = Real(data.to_f32()? as f64);
560                Ok(vec![(value, 1.0)])
561            }
562            Value::Animated(animated_data) => animated_data.sample(shutter, samples),
563        }
564    }
565}
566
567impl Sample<Integer> for Value {
568    fn sample(
569        &self,
570        shutter: &Shutter,
571        samples: NonZeroU16,
572    ) -> Result<Vec<(Integer, SampleWeight)>> {
573        match self {
574            Value::Uniform(data) => {
575                let value = Integer(data.to_i64()?);
576                Ok(vec![(value, 1.0)])
577            }
578            Value::Animated(animated_data) => animated_data.sample(shutter, samples),
579        }
580    }
581}
582
583// Manual Eq implementation for Value
584// This is safe because we handle floating point comparison deterministically
585impl Eq for Value {}
586
587impl Value {
588    /// Hash the value with shutter context for animation-aware caching.
589    ///
590    /// For animated values, this samples at standardized points within the shutter
591    /// range and hashes the interpolated values rather than raw keyframes.
592    /// This provides better cache coherency for animations with different absolute
593    /// times but identical interpolated values.
594    pub fn hash_with_shutter<H: Hasher>(&self, state: &mut H, shutter: &Shutter) {
595        match self {
596            Value::Uniform(data) => {
597                // For uniform values, just use regular hashing.
598                data.hash(state);
599            }
600            Value::Animated(animated) => {
601                // For animated values, sample at standardized points.
602                animated.hash_with_shutter(state, shutter);
603            }
604        }
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611
612    #[cfg(feature = "matrix3")]
613    #[test]
614    fn test_matrix_merge_uniform() {
615        // Create two uniform matrices
616        let m1 = nalgebra::Matrix3::new(2.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 1.0); // Scale by 2
617        let m2 = nalgebra::Matrix3::new(1.0, 0.0, 10.0, 0.0, 1.0, 20.0, 0.0, 0.0, 1.0); // Translate by (10, 20)
618
619        let v1 = Value::uniform(m1);
620        let v2 = Value::uniform(m2);
621
622        // Merge them with multiplication
623        let result = v1
624            .merge_with(&v2, |a, b| match (a, b) {
625                (Data::Matrix3(ma), Data::Matrix3(mb)) => Data::Matrix3(ma.clone() * mb.clone()),
626                _ => a.clone(),
627            })
628            .unwrap();
629
630        // Check result is uniform
631        if let Value::Uniform(Data::Matrix3(result_matrix)) = result {
632            let expected = m1 * m2;
633            assert_eq!(result_matrix.0, expected);
634        } else {
635            panic!("Expected uniform result");
636        }
637    }
638
639    #[cfg(feature = "matrix3")]
640    #[test]
641    fn test_matrix_merge_animated() {
642        use frame_tick::Tick;
643
644        // Create first animated matrix (rotation)
645        let m1_t0 = nalgebra::Matrix3::identity();
646        let m1_t10 = nalgebra::Matrix3::new(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0); // 90 degree rotation
647
648        let v1 = Value::animated([
649            (Tick::from_secs(0.0), m1_t0),
650            (Tick::from_secs(10.0), m1_t10),
651        ])
652        .unwrap();
653
654        // Create second animated matrix (scale)
655        let m2_t5 = nalgebra::Matrix3::new(2.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 1.0);
656        let m2_t15 = nalgebra::Matrix3::new(3.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 1.0);
657
658        let v2 = Value::animated([
659            (Tick::from_secs(5.0), m2_t5),
660            (Tick::from_secs(15.0), m2_t15),
661        ])
662        .unwrap();
663
664        // Merge them
665        let result = v1
666            .merge_with(&v2, |a, b| match (a, b) {
667                (Data::Matrix3(ma), Data::Matrix3(mb)) => Data::Matrix3(ma.clone() * mb.clone()),
668                _ => a.clone(),
669            })
670            .unwrap();
671
672        // Check that result is animated with samples at t=0, 5, 10, 15
673        if let Value::Animated(animated) = result {
674            let times = animated.times();
675            assert_eq!(times.len(), 4);
676            assert!(times.contains(&Tick::from_secs(0.0)));
677            assert!(times.contains(&Tick::from_secs(5.0)));
678            assert!(times.contains(&Tick::from_secs(10.0)));
679            assert!(times.contains(&Tick::from_secs(15.0)));
680        } else {
681            panic!("Expected animated result");
682        }
683    }
684}