@@ -6,7 +6,8 @@ use std::sync::Arc;
66/// Opcode type for trees
77///
88/// This is equivalent to [`Op`](crate::context::Op), but also includes the
9- /// [`RemapAxes`](TreeOp::RemapAxes) operation for lazy remapping.
9+ /// [`RemapAxes`](TreeOp::RemapAxes) and [`TreeOp::RemapAffine`] operations for
10+ /// lazy remapping.
1011#[ derive( Debug ) ]
1112#[ allow( missing_docs) ]
1213pub enum TreeOp {
@@ -19,12 +20,23 @@ pub enum TreeOp {
1920 ///
2021 /// When imported into a `Context`, all `x/y/z` clauses within `target` will
2122 /// be replaced with the provided `x/y/z` trees.
23+ ///
24+ /// If the transform is affine, then `RemapAffine` should be preferred,
25+ /// because it flattens sequences of affine transformations.
2226 RemapAxes {
2327 target : Arc < TreeOp > ,
2428 x : Arc < TreeOp > ,
2529 y : Arc < TreeOp > ,
2630 z : Arc < TreeOp > ,
2731 } ,
32+ /// Lazy affine transforms
33+ ///
34+ /// When imported into a `Context`, the `x/y/z` clauses within `target` will
35+ /// be transformed with the provided affine matrix.
36+ RemapAffine {
37+ target : Arc < TreeOp > ,
38+ mat : nalgebra:: Affine3 < f64 > ,
39+ } ,
2840}
2941
3042impl Drop for TreeOp {
@@ -66,6 +78,9 @@ impl TreeOp {
6678 && matches ! ( * * y, TreeOp :: Const ( ..) )
6779 && matches ! ( * * z, TreeOp :: Const ( ..) )
6880 }
81+ TreeOp :: RemapAffine { target, .. } => {
82+ matches ! ( * * target, TreeOp :: Const ( ..) )
83+ }
6984 }
7085 }
7186
@@ -77,6 +92,9 @@ impl TreeOp {
7792 TreeOp :: RemapAxes { target, x, y, z } => {
7893 [ Some ( target) , Some ( x) , Some ( y) , Some ( z) ]
7994 }
95+ TreeOp :: RemapAffine { target, .. } => {
96+ [ Some ( target) , None , None , None ]
97+ }
8098 }
8199 . into_iter ( )
82100 . flatten ( )
@@ -155,6 +173,9 @@ impl Tree {
155173
156174 /// Remaps the axes of the given tree
157175 ///
176+ /// If the mapping is affine, then [`remap_affine`](Self::remap_affine)
177+ /// should be preferred.
178+ ///
158179 /// The remapping is lazy; it is not evaluated until the tree is imported
159180 /// into a `Context`.
160181 pub fn remap_xyz ( & self , x : Tree , y : Tree , z : Tree ) -> Tree {
@@ -166,6 +187,25 @@ impl Tree {
166187 } ) )
167188 }
168189
190+ /// Performs an affine remapping of the given tree
191+ ///
192+ /// The remapping is lazy; it is not evaluated until the tree is imported
193+ /// into a `Context`.
194+ pub fn remap_affine ( & self , mat : nalgebra:: Affine3 < f64 > ) -> Tree {
195+ // Flatten affine trees
196+ let out = match & * self . 0 {
197+ TreeOp :: RemapAffine { target, mat : next } => TreeOp :: RemapAffine {
198+ target : target. clone ( ) ,
199+ mat : mat * next,
200+ } ,
201+ _ => TreeOp :: RemapAffine {
202+ target : self . 0 . clone ( ) ,
203+ mat,
204+ } ,
205+ } ;
206+ Self ( out. into ( ) )
207+ }
208+
169209 /// Returns the inner [`Var`] if this is an input tree, or `None`
170210 pub fn var ( & self ) -> Option < Var > {
171211 if let TreeOp :: Input ( v) = & * self . 0 {
@@ -399,6 +439,80 @@ mod test {
399439 assert_eq ! ( ctx. eval_xyz( v_, 0.0 , 1.0 , 0.0 ) . unwrap( ) , 4.0 ) ;
400440 }
401441
442+ #[ test]
443+ fn test_remap_affine ( ) {
444+ let s = Tree :: x ( ) ;
445+ // Two rotations by 45° -> 90°
446+ let t = nalgebra:: convert ( nalgebra:: Rotation3 :: < f64 > :: from_axis_angle (
447+ & nalgebra:: Vector3 :: < f64 > :: z_axis ( ) ,
448+ -std:: f64:: consts:: FRAC_PI_4 ,
449+ ) ) ;
450+ let s = s. remap_affine ( t) ;
451+ let s = s. remap_affine ( t) ;
452+
453+ let TreeOp :: RemapAffine { target, .. } = & * s else {
454+ panic ! ( "invalid shape" ) ;
455+ } ;
456+ assert ! ( matches!( & * * target, TreeOp :: Input ( Var :: X ) ) ) ;
457+
458+ let mut ctx = Context :: new ( ) ;
459+ let v_ = ctx. import ( & s) ;
460+
461+ assert ! ( ( ctx. eval_xyz( v_, 0.0 , 1.0 , 0.0 ) . unwrap( ) - 1.0 ) . abs( ) < 1e-6 ) ;
462+ assert ! (
463+ ( ctx. eval_xyz( v_, 0.0 , -2.0 , 0.0 ) . unwrap( ) - -2.0 ) . abs( ) < 1e-6
464+ ) ;
465+ }
466+
467+ #[ test]
468+ fn test_remap_order ( ) {
469+ let translate = nalgebra:: convert ( nalgebra:: Translation3 :: < f64 > :: new (
470+ 3.0 , 0.0 , 0.0 ,
471+ ) ) ;
472+ let scale =
473+ nalgebra:: convert ( nalgebra:: Scale3 :: < f64 > :: new ( 0.5 , 0.5 , 0.5 ) ) ;
474+
475+ let s = Tree :: x ( ) ;
476+ let s = s. remap_affine ( scale) ;
477+ let s = s. remap_affine ( translate) ;
478+
479+ // Confirm that we didn't stack up RemapAffine nodes
480+ let TreeOp :: RemapAffine { target, .. } = & * s else {
481+ panic ! ( "invalid shape" ) ;
482+ } ;
483+ assert ! ( matches!( & * * target, TreeOp :: Input ( Var :: X ) ) ) ;
484+
485+ // Basic evaluation testing
486+ let mut ctx = Context :: new ( ) ;
487+ let v_ = ctx. import ( & s) ;
488+ assert_eq ! ( ctx. eval_xyz( v_, 1.0 , 0.0 , 0.0 ) . unwrap( ) , 3.5 ) ;
489+ assert_eq ! ( ctx. eval_xyz( v_, 2.0 , 0.0 , 0.0 ) . unwrap( ) , 4.0 ) ;
490+
491+ // Do the same thing but testing collapsing in `Context::import`
492+ let manual = TreeOp :: RemapAffine {
493+ target : Arc :: new ( TreeOp :: RemapAffine {
494+ target : TreeOp :: Input ( Var :: X ) . into ( ) ,
495+ mat : scale,
496+ } ) ,
497+ mat : translate,
498+ }
499+ . into ( ) ;
500+ let mut ctx = Context :: new ( ) ;
501+ let v_ = ctx. import ( & manual) ;
502+ assert_eq ! ( ctx. eval_xyz( v_, 1.0 , 0.0 , 0.0 ) . unwrap( ) , 3.5 ) ;
503+ assert_eq ! ( ctx. eval_xyz( v_, 2.0 , 0.0 , 0.0 ) . unwrap( ) , 4.0 ) ;
504+
505+ // Swap the order and make sure it still works
506+ let s = Tree :: x ( ) ;
507+ let s = s. remap_affine ( translate) ;
508+ let s = s. remap_affine ( scale) ;
509+
510+ let mut ctx = Context :: new ( ) ;
511+ let v_ = ctx. import ( & s) ;
512+ assert_eq ! ( ctx. eval_xyz( v_, 1.0 , 0.0 , 0.0 ) . unwrap( ) , 2.0 ) ;
513+ assert_eq ! ( ctx. eval_xyz( v_, 2.0 , 0.0 , 0.0 ) . unwrap( ) , 2.5 ) ;
514+ }
515+
402516 #[ test]
403517 fn deep_recursion_drop ( ) {
404518 let mut x = Tree :: x ( ) ;
0 commit comments