token-value-map 0.2.5

A token-value map with interpolation of values: what you need for DCCs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
use crate::*;
use core::num::NonZeroU16;
use std::hash::{Hash, Hasher};

/// Type alias for bracket sampling return type.
type BracketSample = (Option<(Time, Data)>, Option<(Time, Data)>);

/// A value that can be either uniform or animated over time.
///
/// A [`Value`] contains either a single [`Data`] value that remains constant
/// (uniform) or [`AnimatedData`] that changes over time with interpolation.
#[derive(Clone, Debug, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "facet", derive(Facet))]
#[cfg_attr(feature = "facet", facet(opaque))]
#[cfg_attr(feature = "facet", repr(u8))]
#[cfg_attr(feature = "rkyv", derive(Archive, RkyvSerialize, RkyvDeserialize))]
pub enum Value {
    /// A constant value that does not change over time.
    Uniform(Data),
    /// A value that changes over time with keyframe interpolation.
    Animated(AnimatedData),
}

impl Value {
    /// Create a uniform value that does not change over time.
    pub fn uniform<V: Into<Data>>(value: V) -> Self {
        Value::Uniform(value.into())
    }

    /// Create an animated value from time-value pairs.
    ///
    /// All samples must have the same data type. Vector samples are padded
    /// to match the length of the longest vector in the set.
    pub fn animated<I, V>(samples: I) -> Result<Self>
    where
        I: IntoIterator<Item = (Time, V)>,
        V: Into<Data>,
    {
        let mut samples_vec: Vec<(Time, Data)> =
            samples.into_iter().map(|(t, v)| (t, v.into())).collect();

        if samples_vec.is_empty() {
            return Err(Error::EmptySamples);
        }

        // Get the data type from the first sample
        let data_type = samples_vec[0].1.data_type();

        // Check all samples have the same type and handle length consistency
        let mut expected_len: Option<usize> = None;
        for (time, value) in &mut samples_vec {
            if value.data_type() != data_type {
                return Err(Error::AnimatedTypeMismatch {
                    expected: data_type,
                    got: value.data_type(),
                    time: *time,
                });
            }

            // Check vector length consistency
            if let Some(vec_len) = value.try_len() {
                match expected_len {
                    None => expected_len = Some(vec_len),
                    Some(expected) => {
                        if vec_len > expected {
                            return Err(Error::VectorLengthExceeded {
                                actual: vec_len,
                                expected,
                                time: *time,
                            });
                        } else if vec_len < expected {
                            // Pad to expected length
                            value.pad_to_length(expected);
                        }
                    }
                }
            }
        }

        // Create the appropriate AnimatedData variant by extracting the
        // specific data type

        let animated_data = match data_type {
            DataType::Boolean => {
                let typed_samples: Vec<(Time, Boolean)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Boolean(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Boolean(TimeDataMap::from_iter(typed_samples))
            }
            DataType::Integer => {
                let typed_samples: Vec<(Time, Integer)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Integer(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Integer(TimeDataMap::from_iter(typed_samples))
            }
            DataType::Real => {
                let typed_samples: Vec<(Time, Real)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Real(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Real(TimeDataMap::from_iter(typed_samples))
            }
            DataType::String => {
                let typed_samples: Vec<(Time, String)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::String(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::String(TimeDataMap::from_iter(typed_samples))
            }
            DataType::Color => {
                let typed_samples: Vec<(Time, Color)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Color(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Color(TimeDataMap::from_iter(typed_samples))
            }
            #[cfg(feature = "vector2")]
            DataType::Vector2 => {
                let typed_samples: Vec<(Time, Vector2)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Vector2(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Vector2(TimeDataMap::from_iter(typed_samples))
            }
            #[cfg(feature = "vector3")]
            DataType::Vector3 => {
                let typed_samples: Vec<(Time, Vector3)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Vector3(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Vector3(TimeDataMap::from_iter(typed_samples))
            }
            #[cfg(feature = "matrix3")]
            DataType::Matrix3 => {
                let typed_samples: Vec<(Time, Matrix3)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Matrix3(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Matrix3(TimeDataMap::from_iter(typed_samples))
            }
            #[cfg(feature = "normal3")]
            DataType::Normal3 => {
                let typed_samples: Vec<(Time, Normal3)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Normal3(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Normal3(TimeDataMap::from_iter(typed_samples))
            }
            #[cfg(feature = "point3")]
            DataType::Point3 => {
                let typed_samples: Vec<(Time, Point3)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Point3(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Point3(TimeDataMap::from_iter(typed_samples))
            }
            #[cfg(feature = "matrix4")]
            DataType::Matrix4 => {
                let typed_samples: Vec<(Time, Matrix4)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Matrix4(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Matrix4(TimeDataMap::from_iter(typed_samples))
            }
            DataType::BooleanVec => {
                let typed_samples: Vec<(Time, BooleanVec)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::BooleanVec(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::BooleanVec(TimeDataMap::from_iter(typed_samples))
            }
            DataType::IntegerVec => {
                let typed_samples: Vec<(Time, IntegerVec)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::IntegerVec(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::IntegerVec(TimeDataMap::from_iter(typed_samples))
            }
            DataType::RealVec => {
                let typed_samples: Vec<(Time, RealVec)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::RealVec(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::RealVec(TimeDataMap::from_iter(typed_samples))
            }
            DataType::ColorVec => {
                let typed_samples: Vec<(Time, ColorVec)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::ColorVec(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::ColorVec(TimeDataMap::from_iter(typed_samples))
            }
            DataType::StringVec => {
                let typed_samples: Vec<(Time, StringVec)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::StringVec(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::StringVec(TimeDataMap::from_iter(typed_samples))
            }
            #[cfg(all(feature = "vector2", feature = "vec_variants"))]
            DataType::Vector2Vec => {
                let typed_samples: Vec<(Time, Vector2Vec)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Vector2Vec(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Vector2Vec(TimeDataMap::from_iter(typed_samples))
            }
            #[cfg(all(feature = "vector3", feature = "vec_variants"))]
            DataType::Vector3Vec => {
                let typed_samples: Vec<(Time, Vector3Vec)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Vector3Vec(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Vector3Vec(TimeDataMap::from_iter(typed_samples))
            }
            #[cfg(all(feature = "matrix3", feature = "vec_variants"))]
            DataType::Matrix3Vec => {
                let typed_samples: Vec<(Time, Matrix3Vec)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Matrix3Vec(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Matrix3Vec(TimeDataMap::from_iter(typed_samples))
            }
            #[cfg(all(feature = "normal3", feature = "vec_variants"))]
            DataType::Normal3Vec => {
                let typed_samples: Vec<(Time, Normal3Vec)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Normal3Vec(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Normal3Vec(TimeDataMap::from_iter(typed_samples))
            }
            #[cfg(all(feature = "point3", feature = "vec_variants"))]
            DataType::Point3Vec => {
                let typed_samples: Vec<(Time, Point3Vec)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Point3Vec(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Point3Vec(TimeDataMap::from_iter(typed_samples))
            }
            #[cfg(all(feature = "matrix4", feature = "vec_variants"))]
            DataType::Matrix4Vec => {
                let typed_samples: Vec<(Time, Matrix4Vec)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::Matrix4Vec(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::Matrix4Vec(TimeDataMap::from_iter(typed_samples))
            }
            #[cfg(feature = "curves")]
            DataType::RealCurve => {
                let typed_samples: Vec<(Time, RealCurve)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::RealCurve(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::RealCurve(TimeDataMap::from_iter(typed_samples))
            }
            #[cfg(feature = "curves")]
            DataType::ColorCurve => {
                let typed_samples: Vec<(Time, ColorCurve)> = samples_vec
                    .into_iter()
                    .map(|(t, data)| match data {
                        Data::ColorCurve(v) => (t, v),
                        _ => unreachable!("Type validation should have caught this"),
                    })
                    .collect();
                AnimatedData::ColorCurve(TimeDataMap::from_iter(typed_samples))
            }
        };

        Ok(Value::Animated(animated_data))
    }

    /// Add a sample at a specific time, checking length constraints
    pub fn add_sample<V: Into<Data>>(&mut self, time: Time, val: V) -> Result<()> {
        let value = val.into();

        match self {
            Value::Uniform(_uniform_value) => {
                // Switch to animated and drop/ignore the existing uniform
                // content Create a new animated value with only
                // the new sample
                *self = Value::animated(vec![(time, value)])?;
                Ok(())
            }
            Value::Animated(samples) => {
                let data_type = samples.data_type();
                if value.data_type() != data_type {
                    return Err(Error::SampleTypeMismatch {
                        expected: data_type,
                        got: value.data_type(),
                    });
                }

                // Insert the value using the generic insert method
                samples.try_insert(time, value)
            }
        }
    }

    /// Remove a sample at a specific time.
    ///
    /// Returns the removed value if it existed. For uniform values, this is a
    /// no-op and returns `None`. The last sample in an animated value cannot
    /// be removed (returns `None`). Use [`remove_at_or_to_uniform`](Self::remove_at_or_to_uniform)
    /// to degrade to uniform instead.
    pub fn remove_at(&mut self, time: &Time) -> Option<Data> {
        match self {
            Value::Uniform(_) => None,
            Value::Animated(samples) => samples.remove_at(time),
        }
    }

    /// Deprecated alias for [`remove_at`](Self::remove_at).
    #[deprecated(since = "0.2.2", note = "renamed to `remove_at`")]
    pub fn remove_sample(&mut self, time: &Time) -> Option<Data> {
        self.remove_at(time)
    }

    /// Remove a sample, converting to uniform if it was the last keyframe.
    ///
    /// If the value is uniform, returns `None`. If the removed sample was
    /// the last keyframe, the value degrades from [`Value::Animated`] to
    /// [`Value::Uniform`] with that keyframe's value and returns `None`
    /// (the data is preserved, not lost). Otherwise returns the removed value.
    pub fn remove_at_or_to_uniform(&mut self, time: &Time) -> Option<Data> {
        match self {
            Value::Uniform(_) => None,
            Value::Animated(samples) => {
                let removed = samples.remove_at(time);
                if removed.is_some() {
                    return removed;
                }
                // remove_at returned None -- either key not found or last sample.
                // Check if it's the last sample at this time.
                if samples.sample_at(*time).is_some() {
                    // It's the last sample -- degrade to uniform.
                    let data = samples.interpolate(Time::default());
                    *self = Value::Uniform(data);
                }
                None
            }
        }
    }

    /// Sample value at exact time without interpolation.
    ///
    /// Returns the exact value if it exists at the given time, or `None` if
    /// no sample exists at that time for animated values.
    pub fn sample_at(&self, time: Time) -> Option<Data> {
        match self {
            Value::Uniform(v) => Some(v.clone()),
            Value::Animated(samples) => samples.sample_at(time),
        }
    }

    /// Get the value at or before the given time
    pub fn sample_at_or_before(&self, time: Time) -> Option<Data> {
        match self {
            Value::Uniform(v) => Some(v.clone()),
            Value::Animated(_samples) => {
                // For now, use interpolation at the exact time
                // TODO: Implement proper at-or-before sampling in AnimatedData
                Some(self.interpolate(time))
            }
        }
    }

    /// Get the value at or after the given time
    pub fn sample_at_or_after(&self, time: Time) -> Option<Data> {
        match self {
            Value::Uniform(v) => Some(v.clone()),
            Value::Animated(_samples) => {
                // For now, use interpolation at the exact time
                // TODO: Implement proper at-or-after sampling in AnimatedData
                Some(self.interpolate(time))
            }
        }
    }

    /// Interpolate value at the given time.
    ///
    /// For uniform values, returns the constant value. For animated values,
    /// interpolates between surrounding keyframes using appropriate
    /// interpolation methods (linear, quadratic, or hermite).
    pub fn interpolate(&self, time: Time) -> Data {
        match self {
            Value::Uniform(v) => v.clone(),
            Value::Animated(samples) => samples.interpolate(time),
        }
    }

    /// Get surrounding samples for interpolation.
    pub fn sample_surrounding<const N: usize>(&self, time: Time) -> SmallVec<[(Time, Data); N]> {
        let mut result = SmallVec::<[(Time, Data); N]>::new_const();
        match self {
            Value::Uniform(v) => result.push((time, v.clone())),
            Value::Animated(_samples) => {
                // TODO: Implement proper surrounding sample collection for
                // AnimatedData For now, just return the
                // interpolated value at the given time
                let value = self.interpolate(time);
                result.push((time, value));
            }
        }
        result
    }

    /// Get the two samples surrounding a time for linear interpolation
    pub fn sample_bracket(&self, time: Time) -> BracketSample {
        match self {
            Value::Uniform(v) => (Some((time, v.clone())), None),
            Value::Animated(_samples) => {
                // TODO: Implement proper bracketing for AnimatedData
                // For now, just return the interpolated value at the given time
                let value = self.interpolate(time);
                (Some((time, value)), None)
            }
        }
    }

    /// Check if the value is animated.
    pub fn is_animated(&self) -> bool {
        match self {
            Value::Uniform(_) => false,
            Value::Animated(samples) => samples.is_animated(),
        }
    }

    /// Get the number of time samples.
    pub fn sample_count(&self) -> usize {
        match self {
            Value::Uniform(_) => 1,
            Value::Animated(samples) => samples.len(),
        }
    }

    /// Get all time samples.
    pub fn times(&self) -> SmallVec<[Time; 10]> {
        match self {
            Value::Uniform(_) => SmallVec::<[Time; 10]>::new_const(),
            Value::Animated(samples) => samples.times(),
        }
    }

    /// Get bezier handles at a given time.
    ///
    /// Returns None for uniform values or non-scalar types.
    #[cfg(all(feature = "interpolation", feature = "egui-keyframe"))]
    pub fn bezier_handles(&self, time: &Time) -> Option<egui_keyframe::BezierHandles> {
        match self {
            Value::Uniform(_) => None,
            Value::Animated(samples) => samples.bezier_handles(time),
        }
    }

    /// Set bezier handles at a given time.
    ///
    /// Returns an error for uniform values or non-scalar types.
    #[cfg(all(feature = "interpolation", feature = "egui-keyframe"))]
    pub fn set_bezier_handles(
        &mut self,
        time: &Time,
        handles: egui_keyframe::BezierHandles,
    ) -> Result<()> {
        match self {
            Value::Uniform(_) => Err(Error::InterpolationOnUniform),
            Value::Animated(samples) => samples.set_bezier_handles(time, handles),
        }
    }

    /// Set the interpolation type at a given time.
    ///
    /// Returns an error for uniform values or non-scalar types.
    #[cfg(all(feature = "interpolation", feature = "egui-keyframe"))]
    pub fn set_keyframe_type(
        &mut self,
        time: &Time,
        keyframe_type: egui_keyframe::KeyframeType,
    ) -> Result<()> {
        match self {
            Value::Uniform(_) => Err(Error::InterpolationOnUniform),
            Value::Animated(samples) => samples.set_keyframe_type(time, keyframe_type),
        }
    }

    /// Merge this value with another using a combiner function.
    ///
    /// For uniform values, applies the combiner once.
    /// For animated values, samples both at the union of all keyframe times
    /// and applies the combiner at each time.
    ///
    /// # Example
    /// ```ignore
    /// // Multiply two matrices
    /// let result = matrix1.merge_with(&matrix2, |a, b| {
    ///     match (a, b) {
    ///         (Data::Matrix3(m1), Data::Matrix3(m2)) => {
    ///             Data::Matrix3(Matrix3(m1.0 * m2.0))
    ///         }
    ///         _ => a, // fallback
    ///     }
    /// })?;
    /// ```
    pub fn merge_with<F>(&self, other: &Value, combiner: F) -> Result<Value>
    where
        F: Fn(&Data, &Data) -> Data,
    {
        match (self, other) {
            // Both uniform: simple case
            (Value::Uniform(a), Value::Uniform(b)) => Ok(Value::Uniform(combiner(a, b))),

            // One or both animated: need to sample at union of times
            _ => {
                // Collect all unique times from both values
                let mut all_times = std::collections::BTreeSet::new();

                // Add times from self
                for t in self.times() {
                    all_times.insert(t);
                }

                // Add times from other
                for t in other.times() {
                    all_times.insert(t);
                }

                // If no times found (both were uniform with no times), sample at default
                if all_times.is_empty() {
                    let a = self.interpolate(Time::default());
                    let b = other.interpolate(Time::default());
                    return Ok(Value::Uniform(combiner(&a, &b)));
                }

                // Sample both values at all times and combine
                let mut combined_samples = Vec::new();
                for time in all_times {
                    let a = self.interpolate(time);
                    let b = other.interpolate(time);
                    let combined = combiner(&a, &b);
                    combined_samples.push((time, combined));
                }

                // If only one sample, return as uniform
                if combined_samples.len() == 1 {
                    Ok(Value::Uniform(combined_samples[0].1.clone()))
                } else {
                    // Create animated value from combined samples
                    Value::animated(combined_samples)
                }
            }
        }
    }
}

// From implementations for Value
impl<V: Into<Data>> From<V> for Value {
    fn from(value: V) -> Self {
        Value::uniform(value)
    }
}

// Sample trait implementations for Value using macro
#[cfg(feature = "vector2")]
impl_sample_for_value!(Vector2, Vector2);
#[cfg(feature = "vector3")]
impl_sample_for_value!(Vector3, Vector3);
impl_sample_for_value!(Color, Color);
#[cfg(feature = "matrix3")]
impl_sample_for_value!(Matrix3, Matrix3);
#[cfg(feature = "normal3")]
impl_sample_for_value!(Normal3, Normal3);
#[cfg(feature = "point3")]
impl_sample_for_value!(Point3, Point3);
#[cfg(feature = "matrix4")]
impl_sample_for_value!(Matrix4, Matrix4);

// Special implementations for Real and Integer that handle type conversion
impl Sample<Real> for Value {
    fn sample(&self, shutter: &Shutter, samples: NonZeroU16) -> Result<Vec<(Real, SampleWeight)>> {
        match self {
            Value::Uniform(data) => {
                let value = Real(data.to_f32()? as f64);
                Ok(vec![(value, 1.0)])
            }
            Value::Animated(animated_data) => animated_data.sample(shutter, samples),
        }
    }
}

impl Sample<Integer> for Value {
    fn sample(
        &self,
        shutter: &Shutter,
        samples: NonZeroU16,
    ) -> Result<Vec<(Integer, SampleWeight)>> {
        match self {
            Value::Uniform(data) => {
                let value = Integer(data.to_i64()?);
                Ok(vec![(value, 1.0)])
            }
            Value::Animated(animated_data) => animated_data.sample(shutter, samples),
        }
    }
}

// Manual Eq implementation for Value
// This is safe because we handle floating point comparison deterministically
impl Eq for Value {}

impl Value {
    /// Hash the value with shutter context for animation-aware caching.
    ///
    /// For animated values, this samples at standardized points within the shutter
    /// range and hashes the interpolated values rather than raw keyframes.
    /// This provides better cache coherency for animations with different absolute
    /// times but identical interpolated values.
    pub fn hash_with_shutter<H: Hasher>(&self, state: &mut H, shutter: &Shutter) {
        match self {
            Value::Uniform(data) => {
                // For uniform values, just use regular hashing.
                data.hash(state);
            }
            Value::Animated(animated) => {
                // For animated values, sample at standardized points.
                animated.hash_with_shutter(state, shutter);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(feature = "matrix3")]
    #[test]
    fn test_matrix_merge_uniform() {
        // Create two uniform matrices
        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
        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)

        let v1 = Value::uniform(m1);
        let v2 = Value::uniform(m2);

        // Merge them with multiplication
        let result = v1
            .merge_with(&v2, |a, b| match (a, b) {
                (Data::Matrix3(ma), Data::Matrix3(mb)) => Data::Matrix3(ma.clone() * mb.clone()),
                _ => a.clone(),
            })
            .unwrap();

        // Check result is uniform
        if let Value::Uniform(Data::Matrix3(result_matrix)) = result {
            let expected = m1 * m2;
            assert_eq!(result_matrix.0, expected);
        } else {
            panic!("Expected uniform result");
        }
    }

    #[cfg(feature = "matrix3")]
    #[test]
    fn test_matrix_merge_animated() {
        use frame_tick::Tick;

        // Create first animated matrix (rotation)
        let m1_t0 =
            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
        let m1_t10 =
            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

        let v1 = Value::animated([
            (Tick::from_secs(0.0), m1_t0),
            (Tick::from_secs(10.0), m1_t10),
        ])
        .unwrap();

        // Create second animated matrix (scale)
        let m2_t5 =
            crate::math::mat3_from_row_slice(&[2.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 1.0]);
        let m2_t15 =
            crate::math::mat3_from_row_slice(&[3.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 1.0]);

        let v2 = Value::animated([
            (Tick::from_secs(5.0), m2_t5),
            (Tick::from_secs(15.0), m2_t15),
        ])
        .unwrap();

        // Merge them
        let result = v1
            .merge_with(&v2, |a, b| match (a, b) {
                (Data::Matrix3(ma), Data::Matrix3(mb)) => Data::Matrix3(ma.clone() * mb.clone()),
                _ => a.clone(),
            })
            .unwrap();

        // Check that result is animated with samples at t=0, 5, 10, 15
        if let Value::Animated(animated) = result {
            let times = animated.times();
            assert_eq!(times.len(), 4);
            assert!(times.contains(&Tick::from_secs(0.0)));
            assert!(times.contains(&Tick::from_secs(5.0)));
            assert!(times.contains(&Tick::from_secs(10.0)));
            assert!(times.contains(&Tick::from_secs(15.0)));
        } else {
            panic!("Expected animated result");
        }
    }
}