Skip to main content

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