Skip to content

Commit baef3e3

Browse files
authored
Add affine transforms (#292)
When building math trees with transforms, merging affine transforms helps to keep intervals tight.
1 parent a54a297 commit baef3e3

3 files changed

Lines changed: 159 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
a point with `i32` coordinates (instead of `f32`). This helps us distinguish
1414
between screen (pixel) and world (floating-point) coordinates at the type
1515
level.
16+
- Add `Tree::remap_affine` (and `TreeOp::RemapAffine`) to perform affine
17+
transformations on math expressions. These transformations are composable;
18+
two affine transforms will be combined into a single transform if stacked
19+
together.
1620

1721
# 0.3.5
1822
- Added `#[derive(Serialize, Deserialize)]` to `View2` and `View3`

fidget/src/core/context/mod.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use std::fmt::Write;
3030
use std::io::{BufRead, BufReader, Read};
3131
use std::sync::Arc;
3232

33+
use nalgebra::Matrix4;
3334
use ordered_float::OrderedFloat;
3435

3536
define_index!(Node, "An index in the `Context::ops` map");
@@ -1089,10 +1090,13 @@ impl Context {
10891090
Up(&'a Arc<TreeOp>),
10901091
/// Pops the latest axis frame
10911092
Pop,
1093+
/// Pops the latest affine frame
1094+
PopAffine,
10921095
}
10931096
let mut axes = vec![(self.x(), self.y(), self.z())];
10941097
let mut todo = vec![Action::Down(tree.arc())];
10951098
let mut stack = vec![];
1099+
let mut affine: Vec<Matrix4<f64>> = vec![];
10961100

10971101
// Cache of TreeOp -> Node mapping under a particular frame (axes)
10981102
//
@@ -1145,11 +1149,43 @@ impl Context {
11451149
todo.push(Action::Down(y));
11461150
todo.push(Action::Down(z));
11471151
}
1152+
TreeOp::RemapAffine { target, mat } => {
1153+
let prev = affine
1154+
.last()
1155+
.cloned()
1156+
.unwrap_or(Matrix4::identity());
1157+
let mat = prev * mat.to_homogeneous();
1158+
1159+
// Push either an affine frame or an axis frame,
1160+
// depending on whether the target is also affine
1161+
if matches!(&**target, TreeOp::RemapAffine { .. }) {
1162+
affine.push(mat);
1163+
todo.push(Action::PopAffine);
1164+
} else {
1165+
let (x, y, z) = axes.last().unwrap();
1166+
let mut out = [None; 3];
1167+
for i in 0..3 {
1168+
let a = self.mul(mat[(i, 0)], *x).unwrap();
1169+
let b = self.mul(mat[(i, 1)], *y).unwrap();
1170+
let c = self.mul(mat[(i, 2)], *z).unwrap();
1171+
let d = self.constant(mat[(i, 3)]);
1172+
let ab = self.add(a, b).unwrap();
1173+
let cd = self.add(c, d).unwrap();
1174+
out[i] = Some(self.add(ab, cd).unwrap());
1175+
}
1176+
let [x, y, z] = out.map(Option::unwrap);
1177+
axes.push((x, y, z));
1178+
todo.push(Action::Pop);
1179+
}
1180+
todo.push(Action::Down(target));
1181+
}
11481182
}
11491183
}
11501184
Action::Up(t) => {
11511185
match t.as_ref() {
1152-
TreeOp::Const(..) | TreeOp::Input(..) => unreachable!(),
1186+
TreeOp::Const(..)
1187+
| TreeOp::Input(..)
1188+
| TreeOp::RemapAffine { .. } => unreachable!(),
11531189
TreeOp::Unary(op, ..) => {
11541190
let arg = stack.pop().unwrap();
11551191
let out = self.op_unary(arg, *op).unwrap();
@@ -1195,6 +1231,9 @@ impl Context {
11951231
Action::Pop => {
11961232
axes.pop().unwrap();
11971233
}
1234+
Action::PopAffine => {
1235+
affine.pop().unwrap();
1236+
}
11981237
}
11991238
}
12001239
assert_eq!(stack.len(), 1);

fidget/src/core/context/tree.rs

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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)]
1213
pub 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

3042
impl 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

Comments
 (0)