Skip to main content

agg_rust/
trans_bilinear.rs

1//! Bilinear 2D transformation.
2//!
3//! Port of `agg_trans_bilinear.h` — maps arbitrary quadrilaterals using a
4//! bilinear transformation (includes an x*y term, unlike affine).
5
6use crate::simul_eq::simul_eq_solve;
7
8// ============================================================================
9// TransBilinear
10// ============================================================================
11
12/// Bilinear 2D transformation.
13///
14/// Solves for a 4x2 coefficient matrix that maps points between
15/// two quadrilaterals using bilinear interpolation:
16///
17/// ```text
18/// x' = m[0][0] + m[1][0]*x*y + m[2][0]*x + m[3][0]*y
19/// y' = m[0][1] + m[1][1]*x*y + m[2][1]*x + m[3][1]*y
20/// ```
21///
22/// Port of C++ `trans_bilinear`.
23pub struct TransBilinear {
24    mtx: [[f64; 2]; 4],
25    valid: bool,
26}
27
28impl TransBilinear {
29    /// Create an invalid (uninitialized) transform.
30    pub fn new() -> Self {
31        Self {
32            mtx: [[0.0; 2]; 4],
33            valid: false,
34        }
35    }
36
37    /// Create from arbitrary quadrilateral to quadrilateral mapping.
38    pub fn new_quad_to_quad(src: &[f64; 8], dst: &[f64; 8]) -> Self {
39        let mut t = Self::new();
40        t.quad_to_quad(src, dst);
41        t
42    }
43
44    /// Create a rectangle → quadrilateral mapping.
45    pub fn new_rect_to_quad(x1: f64, y1: f64, x2: f64, y2: f64, quad: &[f64; 8]) -> Self {
46        let mut t = Self::new();
47        t.rect_to_quad(x1, y1, x2, y2, quad);
48        t
49    }
50
51    /// Create a quadrilateral → rectangle mapping.
52    pub fn new_quad_to_rect(quad: &[f64; 8], x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
53        let mut t = Self::new();
54        t.quad_to_rect(quad, x1, y1, x2, y2);
55        t
56    }
57
58    /// Set the transformation from two arbitrary quadrilaterals.
59    ///
60    /// Solves a 4×2 system via Gaussian elimination.
61    pub fn quad_to_quad(&mut self, src: &[f64; 8], dst: &[f64; 8]) {
62        let mut left = [[0.0_f64; 4]; 4];
63        let mut right = [[0.0_f64; 2]; 4];
64
65        for i in 0..4 {
66            let ix = i * 2;
67            let iy = ix + 1;
68            left[i][0] = 1.0;
69            left[i][1] = src[ix] * src[iy];
70            left[i][2] = src[ix];
71            left[i][3] = src[iy];
72
73            right[i][0] = dst[ix];
74            right[i][1] = dst[iy];
75        }
76
77        self.valid = simul_eq_solve(&left, &right, &mut self.mtx);
78    }
79
80    /// Set the direct transformation: rectangle → quadrilateral.
81    pub fn rect_to_quad(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, quad: &[f64; 8]) {
82        let src = [x1, y1, x2, y1, x2, y2, x1, y2];
83        self.quad_to_quad(&src, quad);
84    }
85
86    /// Set the reverse transformation: quadrilateral → rectangle.
87    pub fn quad_to_rect(&mut self, quad: &[f64; 8], x1: f64, y1: f64, x2: f64, y2: f64) {
88        let dst = [x1, y1, x2, y1, x2, y2, x1, y2];
89        self.quad_to_quad(quad, &dst);
90    }
91
92    /// Check if the equations were solved successfully.
93    pub fn is_valid(&self) -> bool {
94        self.valid
95    }
96
97    /// Transform a point (x, y).
98    pub fn transform(&self, x: &mut f64, y: &mut f64) {
99        let tx = *x;
100        let ty = *y;
101        let xy = tx * ty;
102        *x = self.mtx[0][0] + self.mtx[1][0] * xy + self.mtx[2][0] * tx + self.mtx[3][0] * ty;
103        *y = self.mtx[0][1] + self.mtx[1][1] * xy + self.mtx[2][1] * tx + self.mtx[3][1] * ty;
104    }
105
106    /// Create an incremental iterator for scanline walking.
107    pub fn begin(&self, x: f64, y: f64, step: f64) -> IteratorX {
108        IteratorX::new(x, y, step, &self.mtx)
109    }
110}
111
112impl Default for TransBilinear {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118// ============================================================================
119// IteratorX — incremental scanline walker
120// ============================================================================
121
122/// Incremental iterator for scanline walking with bilinear transform.
123///
124/// Port of C++ `trans_bilinear::iterator_x`.
125pub struct IteratorX {
126    inc_x: f64,
127    inc_y: f64,
128    pub x: f64,
129    pub y: f64,
130}
131
132impl IteratorX {
133    fn new(tx: f64, ty: f64, step: f64, m: &[[f64; 2]; 4]) -> Self {
134        Self {
135            inc_x: m[1][0] * step * ty + m[2][0] * step,
136            inc_y: m[1][1] * step * ty + m[2][1] * step,
137            x: m[0][0] + m[1][0] * tx * ty + m[2][0] * tx + m[3][0] * ty,
138            y: m[0][1] + m[1][1] * tx * ty + m[2][1] * tx + m[3][1] * ty,
139        }
140    }
141
142    /// Advance to the next step.
143    pub fn next(&mut self) {
144        self.x += self.inc_x;
145        self.y += self.inc_y;
146    }
147}
148
149// ============================================================================
150// Tests
151// ============================================================================
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_identity_rect() {
159        // Rectangle to same rectangle → identity transform
160        let src = [0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0];
161        let dst = [0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0];
162        let t = TransBilinear::new_quad_to_quad(&src, &dst);
163        assert!(t.is_valid());
164        let mut x = 0.5;
165        let mut y = 0.5;
166        t.transform(&mut x, &mut y);
167        assert!((x - 0.5).abs() < 1e-10);
168        assert!((y - 0.5).abs() < 1e-10);
169    }
170
171    #[test]
172    fn test_rect_to_quad_corners() {
173        // Map unit square to a specific quad
174        let quad = [10.0, 10.0, 20.0, 10.0, 20.0, 20.0, 10.0, 20.0];
175        let t = TransBilinear::new_rect_to_quad(0.0, 0.0, 1.0, 1.0, &quad);
176        assert!(t.is_valid());
177
178        // Corner (0,0) → (10,10)
179        let mut x = 0.0;
180        let mut y = 0.0;
181        t.transform(&mut x, &mut y);
182        assert!((x - 10.0).abs() < 1e-10);
183        assert!((y - 10.0).abs() < 1e-10);
184
185        // Corner (1,1) → (20,20)
186        x = 1.0;
187        y = 1.0;
188        t.transform(&mut x, &mut y);
189        assert!((x - 20.0).abs() < 1e-10);
190        assert!((y - 20.0).abs() < 1e-10);
191    }
192
193    #[test]
194    fn test_quad_to_rect() {
195        // Reverse: quad → unit square
196        let quad = [10.0, 10.0, 20.0, 10.0, 20.0, 20.0, 10.0, 20.0];
197        let t = TransBilinear::new_quad_to_rect(&quad, 0.0, 0.0, 1.0, 1.0);
198        assert!(t.is_valid());
199
200        let mut x = 10.0;
201        let mut y = 10.0;
202        t.transform(&mut x, &mut y);
203        assert!((x - 0.0).abs() < 1e-10);
204        assert!((y - 0.0).abs() < 1e-10);
205    }
206
207    #[test]
208    fn test_round_trip_parallelogram() {
209        // Parallelogram (affine case): round-trip should be exact
210        let quad = [5.0, 0.0, 15.0, 2.0, 17.0, 12.0, 7.0, 10.0];
211        let forward = TransBilinear::new_rect_to_quad(0.0, 0.0, 10.0, 10.0, &quad);
212        let reverse = TransBilinear::new_quad_to_rect(&quad, 0.0, 0.0, 10.0, 10.0);
213        assert!(forward.is_valid());
214        assert!(reverse.is_valid());
215
216        let mut x = 5.0;
217        let mut y = 5.0;
218        forward.transform(&mut x, &mut y);
219        reverse.transform(&mut x, &mut y);
220        assert!((x - 5.0).abs() < 1e-8);
221        assert!((y - 5.0).abs() < 1e-8);
222    }
223
224    #[test]
225    fn test_iterator() {
226        let src = [0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0];
227        let dst = [0.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0, 10.0];
228        let t = TransBilinear::new_quad_to_quad(&src, &dst);
229
230        let mut it = t.begin(0.0, 0.0, 0.5);
231        assert!((it.x - 0.0).abs() < 1e-10);
232        assert!((it.y - 0.0).abs() < 1e-10);
233        it.next();
234        assert!((it.x - 5.0).abs() < 1e-10);
235        assert!((it.y - 0.0).abs() < 1e-10);
236    }
237
238    #[test]
239    fn test_default_is_invalid() {
240        let t = TransBilinear::new();
241        assert!(!t.is_valid());
242    }
243
244    #[test]
245    fn test_scaling_transform() {
246        // Map [0,0,1,1] → [0,0,2,2] — should scale by 2
247        let quad = [0.0, 0.0, 2.0, 0.0, 2.0, 2.0, 0.0, 2.0];
248        let t = TransBilinear::new_rect_to_quad(0.0, 0.0, 1.0, 1.0, &quad);
249        assert!(t.is_valid());
250
251        let mut x = 0.5;
252        let mut y = 0.5;
253        t.transform(&mut x, &mut y);
254        assert!((x - 1.0).abs() < 1e-10);
255        assert!((y - 1.0).abs() < 1e-10);
256    }
257
258    #[test]
259    fn test_translation_transform() {
260        // Map [0,0,1,1] → [10,20,11,21] — should translate by (10,20)
261        let quad = [10.0, 20.0, 11.0, 20.0, 11.0, 21.0, 10.0, 21.0];
262        let t = TransBilinear::new_rect_to_quad(0.0, 0.0, 1.0, 1.0, &quad);
263        assert!(t.is_valid());
264
265        let mut x = 0.0;
266        let mut y = 0.0;
267        t.transform(&mut x, &mut y);
268        assert!((x - 10.0).abs() < 1e-10);
269        assert!((y - 20.0).abs() < 1e-10);
270    }
271}