Skip to main content

ranim_core/
transformed.rs

1//! A wrapper that attaches a typed external transform to an item.
2
3use std::ops::Range;
4
5use glam::{DAffine3, DVec3, dvec3};
6
7use crate::{
8    anchor::{Aabb, Locate},
9    color::{AlphaColor, Srgb},
10    components::width::Width,
11    traits::{
12        Alignable, ApplyTransform, Diag, Empty, FillColor, Interpolatable, Opacity, Partial, Rigid,
13        Similarity, StrokeColor, StrokeWidth, TransformGroup, Translation,
14    },
15};
16
17/// An item paired with an external transform represented by `G`.
18///
19/// `inner` retains its own representation while `transform` controls the
20/// extracted geometry. Composition is explicit about order:
21///
22/// - [`compose_outer`](Self::compose_outer): `transform = outer * transform`;
23/// - [`compose_inner`](Self::compose_inner): `transform = transform * inner`.
24///
25/// Applying an `H` through [`ApplyTransform`] is outer composition and is only
26/// available when `H` embeds into the existing storage type (`G: From<H>`).
27/// Operations never widen the wrapper automatically. Widen explicitly with
28/// [`Into::into`] when a more general storage type is required.
29///
30/// ```compile_fail
31/// use ranim_core::{glam::DVec3, prelude::*};
32///
33/// let mut item = ().transformed(Similarity::IDENTITY);
34/// // `Diag` does not embed into `Similarity`; choose `DAffine3` storage first.
35/// item.scale(DVec3::new(2.0, 1.0, 1.0));
36/// ```
37///
38/// Evaluation preserves the typed wrapper. [`Aabb`] calculation converts `G`
39/// to [`DAffine3`] at the geometry boundary. Projective transforms remain
40/// outside the model-transform system.
41#[derive(Debug, Clone, PartialEq)]
42pub struct Transformed<T, G> {
43    /// The item expressed in its own coordinates.
44    pub inner: T,
45    /// The external transform applied to `inner`.
46    pub transform: G,
47}
48
49impl<T, G> Transformed<T, G> {
50    /// Pair `inner` with `transform` without converting either value.
51    pub fn new(inner: T, transform: G) -> Self {
52        Self { inner, transform }
53    }
54
55    /// Map the wrapped item to a new type, keeping `transform` unchanged.
56    pub fn map_inner<U, F>(self, f: F) -> Transformed<U, G>
57    where
58        F: FnOnce(T) -> U,
59    {
60        Transformed::new(f(self.inner), self.transform)
61    }
62
63    /// Map the transform storage to a new type while keeping `inner`
64    /// unchanged.
65    ///
66    /// This is the general form of converting between transform groups —
67    /// for example widening a placement or checked-narrowing an affine
68    /// storage back to a subgroup. Lossless upward conversions need no
69    /// closure at all: [`Transformed`] implements
70    /// `From<Transformed<T, X>> for Transformed<T, Y>` whenever `X` embeds
71    /// into `Y`, so prefer plain `.into()` there.
72    pub fn map_transform<H, F>(self, f: F) -> Transformed<T, H>
73    where
74        F: FnOnce(G) -> H,
75    {
76        Transformed::new(self.inner, f(self.transform))
77    }
78
79    /// Compose `outer` on the left: `transform = outer * transform`.
80    pub fn compose_outer<H>(&mut self, outer: H) -> &mut Self
81    where
82        G: TransformGroup + From<H>,
83    {
84        self.transform = G::from(outer).compose(&self.transform);
85        self
86    }
87
88    /// Compose `inner` on the right: `transform = transform * inner`.
89    pub fn compose_inner<H>(&mut self, inner: H) -> &mut Self
90    where
91        G: TransformGroup + From<H>,
92    {
93        self.transform = self.transform.compose(&G::from(inner));
94        self
95    }
96
97    /// Bake the stored transform into `inner` and remove the wrapper.
98    ///
99    /// This method exists only when `T` directly supports the wrapper's exact
100    /// transform representation `G`.
101    pub fn bake(self) -> T
102    where
103        T: ApplyTransform<G>,
104    {
105        let mut inner = self.inner;
106        inner.apply(self.transform);
107        inner
108    }
109}
110
111impl<T, G, H> ApplyTransform<H> for Transformed<T, G>
112where
113    G: TransformGroup + From<H>,
114{
115    fn apply(&mut self, transform: H) -> &mut Self {
116        self.compose_outer(transform)
117    }
118}
119
120impl<T: Interpolatable, G: Interpolatable> Interpolatable for Transformed<T, G> {
121    fn lerp(&self, target: &Self, t: f64) -> Self {
122        Self {
123            inner: self.inner.lerp(&target.inner, t),
124            transform: self.transform.lerp(&target.transform, t),
125        }
126    }
127}
128
129impl<T: Alignable, G: Clone> Alignable for Transformed<T, G> {
130    fn is_aligned(&self, other: &Self) -> bool {
131        self.inner.is_aligned(&other.inner)
132    }
133
134    fn align_with(&mut self, other: &mut Self) {
135        self.inner.align_with(&mut other.inner);
136    }
137}
138
139impl<T, G> Locate<Transformed<T, G>> for crate::anchor::Centroid
140where
141    crate::anchor::Centroid: Locate<T>,
142    G: Clone + Into<DAffine3>,
143{
144    fn locate(&self, target: &Transformed<T, G>) -> DVec3 {
145        target
146            .transform
147            .clone()
148            .into()
149            .transform_point3(self.locate(&target.inner))
150    }
151}
152
153impl<T, G> Aabb for Transformed<T, G>
154where
155    T: Aabb,
156    G: Clone + Into<DAffine3>,
157{
158    fn aabb(&self) -> [DVec3; 2] {
159        let [min, max] = self.inner.aabb();
160        let transform = self.transform.clone().into();
161        let mut lo = DVec3::splat(f64::INFINITY);
162        let mut hi = DVec3::splat(f64::NEG_INFINITY);
163        for i in 0..8 {
164            let corner = dvec3(
165                if i & 1 == 0 { min.x } else { max.x },
166                if i & 2 == 0 { min.y } else { max.y },
167                if i & 4 == 0 { min.z } else { max.z },
168            );
169            let point = transform.transform_point3(corner);
170            lo = lo.min(point);
171            hi = hi.max(point);
172        }
173        [lo, hi]
174    }
175}
176
177impl<T: Opacity, G> Opacity for Transformed<T, G> {
178    fn set_opacity(&mut self, opacity: f32) -> &mut Self {
179        self.inner.set_opacity(opacity);
180        self
181    }
182}
183
184impl<T: FillColor, G> FillColor for Transformed<T, G> {
185    fn fill_color(&self) -> AlphaColor<Srgb> {
186        self.inner.fill_color()
187    }
188
189    fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
190        self.inner.set_fill_color(color);
191        self
192    }
193
194    fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
195        self.inner.set_fill_opacity(opacity);
196        self
197    }
198}
199
200impl<T: StrokeColor, G> StrokeColor for Transformed<T, G> {
201    fn stroke_color(&self) -> AlphaColor<Srgb> {
202        self.inner.stroke_color()
203    }
204
205    fn set_stroke_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
206        self.inner.set_stroke_color(color);
207        self
208    }
209
210    fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
211        self.inner.set_stroke_opacity(opacity);
212        self
213    }
214}
215
216impl<T: StrokeWidth, G> StrokeWidth for Transformed<T, G> {
217    fn stroke_width(&self) -> f32 {
218        self.inner.stroke_width()
219    }
220
221    fn apply_stroke_func(&mut self, f: impl for<'a> Fn(&'a mut [Width])) -> &mut Self {
222        self.inner.apply_stroke_func(f);
223        self
224    }
225}
226
227impl<T: Partial, G: Clone> Partial for Transformed<T, G> {
228    /// Take a partial slice of the wrapped item, keeping the pose.
229    ///
230    /// The range is forwarded to `inner`, so the partial geometry is taken in
231    /// the item's own coordinates, while the stored transform `G` is cloned
232    /// unchanged. The wrapper therefore keeps its placement while the visible
233    /// geometry grows from the slice, which is what create-style animations
234    /// expect.
235    fn get_partial(&self, range: Range<f64>) -> Self {
236        Transformed::new(self.inner.get_partial(range), self.transform.clone())
237    }
238
239    /// Take a closed partial slice of the wrapped item, keeping the pose.
240    ///
241    /// Like [`get_partial`](Self::get_partial), but the inner slice is closed
242    /// so partially shown curves have caps at both ends; the wrapper's
243    /// transform is preserved.
244    fn get_partial_closed(&self, range: Range<f64>) -> Self {
245        Transformed::new(self.inner.get_partial_closed(range), self.transform.clone())
246    }
247}
248
249impl<T: Empty, G: TransformGroup> Empty for Transformed<T, G> {
250    /// Create an empty placeholder of a wrapped item.
251    ///
252    /// The inner item contributes `T::empty()` while the pose is `G::identity()`:
253    /// an empty placeholder carries no geometry, so there is no meaningful
254    /// placement to preserve — identity keeps it neutral for composition until
255    /// real content is placed on it.
256    fn empty() -> Self {
257        Transformed::new(T::empty(), G::identity())
258    }
259}
260
261macro_rules! impl_transformed_widening {
262    ($source:ty => $target:ty) => {
263        impl<T> From<Transformed<T, $source>> for Transformed<T, $target> {
264            fn from(value: Transformed<T, $source>) -> Self {
265                Self {
266                    inner: value.inner,
267                    transform: value.transform.into(),
268                }
269            }
270        }
271    };
272}
273
274impl_transformed_widening!(Translation => Rigid);
275impl_transformed_widening!(Translation => Similarity);
276impl_transformed_widening!(Translation => DAffine3);
277impl_transformed_widening!(Rigid => Similarity);
278impl_transformed_widening!(Rigid => DAffine3);
279impl_transformed_widening!(Similarity => DAffine3);
280impl_transformed_widening!(Diag => DAffine3);
281
282/// Extension methods for attaching an exact transform representation to a value.
283pub trait TransformedExt: Sized {
284    /// Return `self` wrapped with `transform` stored exactly as `G`.
285    fn transformed<G>(self, transform: G) -> Transformed<Self, G> {
286        Transformed::new(self, transform)
287    }
288}
289
290impl<T> TransformedExt for T {}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use crate::{
296        RanimScene,
297        animation::{build::PlaybackExt, eval::Static},
298        frame::EvaluatedFrame,
299        traits::{ShiftTransform, UniformScaleTransform},
300    };
301    use glam::DQuat;
302
303    #[derive(Debug, Clone, PartialEq)]
304    struct LocalGeometry {
305        points: Vec<DVec3>,
306        transform: DAffine3,
307    }
308
309    fn assert_affine_eq(actual: DAffine3, expected: DAffine3) {
310        assert!(actual.matrix3.abs_diff_eq(expected.matrix3, 1e-9));
311        assert!(actual.translation.abs_diff_eq(expected.translation, 1e-9));
312    }
313
314    #[test]
315    fn evaluation_preserves_the_typed_wrapper_and_local_geometry() {
316        let geometry = LocalGeometry {
317            points: vec![DVec3::ZERO; 3],
318            transform: DAffine3::from_translation(DVec3::X),
319        };
320        let wrapped = geometry.transformed(Translation(dvec3(0.0, 2.0, 0.0)));
321        let mut scene = RanimScene::new();
322        scene.play(Static(wrapped.clone()).with_duration(1.0));
323        let mut frame = EvaluatedFrame::new();
324        scene.seal().eval_at_sec(0.5, &mut frame);
325
326        let items = frame.items::<Transformed<LocalGeometry, Translation>>();
327        assert_eq!(items.len(), 1);
328        assert_eq!(items[0].value, wrapped);
329        assert_eq!(
330            items[0].value.inner.transform,
331            DAffine3::from_translation(DVec3::X)
332        );
333        assert_eq!(items[0].value.inner.points, vec![DVec3::ZERO; 3]);
334        assert!(frame.items::<LocalGeometry>().is_empty());
335    }
336
337    #[test]
338    fn transforms_compose_inside_out() {
339        let inner = vec![DVec3::X].transformed(Translation(DVec3::X));
340        let outer = inner.transformed(Diag(DVec3::splat(2.0)));
341        assert_eq!(outer.inner.inner, vec![DVec3::X]);
342        assert_affine_eq(
343            DAffine3::from(outer.transform) * DAffine3::from(outer.inner.transform),
344            DAffine3::from_scale(DVec3::splat(2.0)) * DAffine3::from_translation(DVec3::X),
345        );
346
347        let mut wrapped = ().transformed(DAffine3::IDENTITY);
348        wrapped.compose_outer(Translation(DVec3::X));
349        wrapped.compose_inner(Diag(DVec3::splat(2.0)));
350        assert_affine_eq(
351            wrapped.transform,
352            DAffine3::from_translation(DVec3::X) * DAffine3::from_scale(DVec3::splat(2.0)),
353        );
354
355        wrapped.apply(Translation(DVec3::Y));
356        assert_affine_eq(
357            wrapped.transform,
358            DAffine3::from_translation(DVec3::Y)
359                * DAffine3::from_translation(DVec3::X)
360                * DAffine3::from_scale(DVec3::splat(2.0)),
361        );
362    }
363
364    #[test]
365    fn interpolation_lerps_inner_and_pose_by_storage_group() {
366        let a = 0.0f64.transformed(Similarity::IDENTITY);
367        let b = 2.0f64.transformed(Similarity {
368            scale: 3.0,
369            rotation: DQuat::IDENTITY,
370            translation: dvec3(2.0, 0.0, 0.0),
371        });
372        let mid = a.lerp(&b, 0.5);
373        assert_eq!(mid.inner, 1.0);
374        assert_eq!(mid.transform.scale, 2.0);
375        assert_eq!(mid.transform.translation, DVec3::X);
376
377        let points = vec![DVec3::X];
378        let a = points.clone().transformed(Translation(DVec3::X));
379        let b = points.transformed(Translation(dvec3(-1.0, 2.0, 0.0)));
380        let mid = a.lerp(&b, 0.5);
381        // Local geometry never moves; only the pose interpolates.
382        assert_eq!(mid.inner, vec![DVec3::X]);
383        assert_affine_eq(
384            DAffine3::from(mid.transform),
385            DAffine3::from_translation(dvec3(0.0, 1.0, 0.0)),
386        );
387
388        let a = 42.0f64.transformed(Rigid::IDENTITY);
389        let b = 42.0f64.transformed(Rigid::from_axis_angle(
390            DVec3::Z,
391            std::f64::consts::FRAC_PI_2,
392        ));
393        let mid = a.lerp(&b, 0.5);
394        let rotated = mid.transform.rotation * DVec3::X;
395        let expected = DQuat::from_axis_angle(DVec3::Z, std::f64::consts::FRAC_PI_4) * DVec3::X;
396        assert!(rotated.abs_diff_eq(expected, 1e-9));
397    }
398
399    #[test]
400    fn anchor_queries_apply_the_outer_pose() {
401        let wrapped = dvec3(1.0, 1.0, 1.0).transformed(Translation(dvec3(10.0, 0.0, 0.0)));
402        assert_eq!(wrapped.aabb(), [dvec3(11.0, 1.0, 1.0); 2]);
403
404        let wrapped = dvec3(1.0, 2.0, 3.0).transformed(Translation(dvec3(4.0, 5.0, 6.0)));
405        assert_eq!(
406            crate::anchor::Centroid.locate(&wrapped),
407            dvec3(5.0, 7.0, 9.0)
408        );
409    }
410
411    #[test]
412    fn map_operations_preserve_pose_and_support_storage_conversion() {
413        let mapped = vec![DVec3::ZERO; 3]
414            .transformed(Translation(DVec3::X))
415            .map_inner(|points| points.len())
416            .map_transform(DAffine3::from);
417        assert_eq!(mapped.inner, 3);
418        assert_eq!(mapped.transform, DAffine3::from_translation(DVec3::X));
419    }
420
421    #[test]
422    fn subgroup_operations_keep_the_same_storage_type() {
423        fn require_similarity<T>(_: &Transformed<T, Similarity>) {}
424
425        let mut wrapped = ().transformed(Similarity::IDENTITY);
426        wrapped.shift(DVec3::X).scale_uniform(2.0);
427        require_similarity(&wrapped);
428        assert_eq!(wrapped.transform.scale, 2.0);
429        assert_eq!(wrapped.transform.translation, dvec3(2.0, 0.0, 0.0));
430    }
431
432    #[test]
433    fn explicit_widening_preserves_numeric_transform() {
434        let translation = ().transformed(Translation(dvec3(1.0, 2.0, 3.0)));
435        let rigid: Transformed<_, Rigid> = translation.clone().into();
436        let similarity: Transformed<_, Similarity> = translation.clone().into();
437        let affine: Transformed<_, DAffine3> = translation.into();
438        assert_affine_eq(rigid.transform.into(), affine.transform);
439        assert_affine_eq(similarity.transform.into(), affine.transform);
440
441        let rigid = ().transformed(Rigid {
442            rotation: DQuat::from_rotation_z(0.7),
443            translation: DVec3::Y,
444        });
445        let similarity: Transformed<_, Similarity> = rigid.clone().into();
446        let affine: Transformed<_, DAffine3> = rigid.into();
447        assert_affine_eq(similarity.transform.into(), affine.transform);
448
449        let similarity = ().transformed(Similarity {
450            scale: 2.5,
451            rotation: DQuat::from_rotation_x(0.4),
452            translation: DVec3::Z,
453        });
454        let affine: Transformed<_, DAffine3> = similarity.into();
455        assert_affine_eq(
456            affine.transform,
457            DAffine3::from_scale_rotation_translation(
458                DVec3::splat(2.5),
459                DQuat::from_rotation_x(0.4),
460                DVec3::Z,
461            ),
462        );
463
464        let diagonal = ().transformed(Diag(dvec3(2.0, 0.0, -3.0)));
465        let affine_diagonal: Transformed<_, DAffine3> = diagonal.into();
466        assert_eq!(affine_diagonal.transform.matrix3.y_axis, DVec3::ZERO);
467        assert_eq!(affine_diagonal.transform.matrix3.z_axis.z, -3.0);
468    }
469
470    /// A minimal stand-in for the geometry traits: `ranim-core` itself has no
471    /// concrete `Partial` type, so a partial slice is modelled by recording
472    /// the requested range and whether it was closed.
473    #[derive(Clone, Debug, PartialEq)]
474    struct StubShape {
475        /// The range this shape was last materialized from.
476        range: Range<f64>,
477        closed: bool,
478    }
479
480    impl Partial for StubShape {
481        fn get_partial(&self, range: Range<f64>) -> Self {
482            Self {
483                range,
484                closed: false,
485            }
486        }
487
488        fn get_partial_closed(&self, range: Range<f64>) -> Self {
489            Self {
490                range,
491                closed: true,
492            }
493        }
494    }
495
496    impl Empty for StubShape {
497        fn empty() -> Self {
498            Self {
499                range: 0.0..0.0,
500                closed: false,
501            }
502        }
503    }
504
505    #[test]
506    fn partial_forwards_the_range_and_keeps_the_pose() {
507        let wrapped = StubShape {
508            range: 0.0..1.0,
509            closed: false,
510        }
511        .transformed(Translation(DVec3::X));
512
513        let partial = wrapped.get_partial(0.2..0.8);
514        // The very same range reaches the inner item.
515        assert_eq!(partial.inner.range, 0.2..0.8);
516        assert!(!partial.inner.closed);
517        // The stored pose is cloned untouched.
518        assert_eq!(partial.transform, wrapped.transform);
519
520        let closed = wrapped.get_partial_closed(0.3..0.6);
521        assert_eq!(closed.inner.range, 0.3..0.6);
522        assert!(closed.inner.closed);
523        assert_eq!(closed.transform, wrapped.transform);
524    }
525
526    #[test]
527    fn empty_wrapper_carries_an_identity_pose() {
528        let wrapped: Transformed<StubShape, Translation> = Empty::empty();
529        assert_eq!(wrapped.inner, StubShape::empty());
530        assert_eq!(wrapped.transform, Translation(DVec3::ZERO));
531    }
532}