Skip to main content

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