Skip to main content

agg_rust/
trans_viewport.rs

1//! Viewport transformation.
2//!
3//! Port of `agg_trans_viewport.h` — simple orthogonal transformation from
4//! world coordinates to device (screen) coordinates with aspect ratio control.
5
6use crate::trans_affine::TransAffine;
7
8// ============================================================================
9// AspectRatio
10// ============================================================================
11
12/// Aspect ratio handling mode for viewport transformation.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum AspectRatio {
15    /// Stretch to fill — no aspect ratio preservation.
16    Stretch,
17    /// Meet — fit entirely within device viewport (letterbox/pillarbox).
18    Meet,
19    /// Slice — fill device viewport entirely (may crop).
20    Slice,
21}
22
23// ============================================================================
24// TransViewport
25// ============================================================================
26
27/// Viewport transformer.
28///
29/// Maps world coordinates to device coordinates with optional aspect ratio
30/// preservation. Can produce a `TransAffine` matrix for use with other
31/// components.
32///
33/// Port of C++ `trans_viewport`.
34pub struct TransViewport {
35    world_x1: f64,
36    world_y1: f64,
37    world_x2: f64,
38    world_y2: f64,
39    device_x1: f64,
40    device_y1: f64,
41    device_x2: f64,
42    device_y2: f64,
43    aspect: AspectRatio,
44    is_valid: bool,
45    align_x: f64,
46    align_y: f64,
47    // Computed values
48    wx1: f64,
49    wy1: f64,
50    wx2: f64,
51    wy2: f64,
52    dx1: f64,
53    dy1: f64,
54    kx: f64,
55    ky: f64,
56}
57
58impl Default for TransViewport {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl TransViewport {
65    pub fn new() -> Self {
66        Self {
67            world_x1: 0.0,
68            world_y1: 0.0,
69            world_x2: 1.0,
70            world_y2: 1.0,
71            device_x1: 0.0,
72            device_y1: 0.0,
73            device_x2: 1.0,
74            device_y2: 1.0,
75            aspect: AspectRatio::Stretch,
76            is_valid: true,
77            align_x: 0.5,
78            align_y: 0.5,
79            wx1: 0.0,
80            wy1: 0.0,
81            wx2: 1.0,
82            wy2: 1.0,
83            dx1: 0.0,
84            dy1: 0.0,
85            kx: 1.0,
86            ky: 1.0,
87        }
88    }
89
90    /// Set aspect ratio preservation mode and alignment.
91    pub fn preserve_aspect_ratio(&mut self, align_x: f64, align_y: f64, aspect: AspectRatio) {
92        self.align_x = align_x;
93        self.align_y = align_y;
94        self.aspect = aspect;
95        self.update();
96    }
97
98    /// Set the device (screen) viewport rectangle.
99    pub fn set_device_viewport(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
100        self.device_x1 = x1;
101        self.device_y1 = y1;
102        self.device_x2 = x2;
103        self.device_y2 = y2;
104        self.update();
105    }
106
107    /// Set the world (logical) viewport rectangle.
108    pub fn set_world_viewport(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
109        self.world_x1 = x1;
110        self.world_y1 = y1;
111        self.world_x2 = x2;
112        self.world_y2 = y2;
113        self.update();
114    }
115
116    /// Get the device viewport rectangle.
117    pub fn device_viewport(&self) -> (f64, f64, f64, f64) {
118        (
119            self.device_x1,
120            self.device_y1,
121            self.device_x2,
122            self.device_y2,
123        )
124    }
125
126    /// Get the world viewport rectangle.
127    pub fn world_viewport(&self) -> (f64, f64, f64, f64) {
128        (self.world_x1, self.world_y1, self.world_x2, self.world_y2)
129    }
130
131    /// Get the actual (computed) world viewport after aspect ratio adjustment.
132    pub fn world_viewport_actual(&self) -> (f64, f64, f64, f64) {
133        (self.wx1, self.wy1, self.wx2, self.wy2)
134    }
135
136    pub fn is_valid(&self) -> bool {
137        self.is_valid
138    }
139
140    pub fn align_x(&self) -> f64 {
141        self.align_x
142    }
143
144    pub fn align_y(&self) -> f64 {
145        self.align_y
146    }
147
148    pub fn aspect_ratio(&self) -> AspectRatio {
149        self.aspect
150    }
151
152    /// Transform world coordinates to device coordinates.
153    pub fn transform(&self, x: &mut f64, y: &mut f64) {
154        *x = (*x - self.wx1) * self.kx + self.dx1;
155        *y = (*y - self.wy1) * self.ky + self.dy1;
156    }
157
158    /// Transform only the scale component (no translation).
159    pub fn transform_scale_only(&self, x: &mut f64, y: &mut f64) {
160        *x *= self.kx;
161        *y *= self.ky;
162    }
163
164    /// Transform device coordinates back to world coordinates.
165    pub fn inverse_transform(&self, x: &mut f64, y: &mut f64) {
166        *x = (*x - self.dx1) / self.kx + self.wx1;
167        *y = (*y - self.dy1) / self.ky + self.wy1;
168    }
169
170    /// Inverse transform only the scale component.
171    pub fn inverse_transform_scale_only(&self, x: &mut f64, y: &mut f64) {
172        *x /= self.kx;
173        *y /= self.ky;
174    }
175
176    pub fn device_dx(&self) -> f64 {
177        self.dx1 - self.wx1 * self.kx
178    }
179
180    pub fn device_dy(&self) -> f64 {
181        self.dy1 - self.wy1 * self.ky
182    }
183
184    pub fn scale_x(&self) -> f64 {
185        self.kx
186    }
187
188    pub fn scale_y(&self) -> f64 {
189        self.ky
190    }
191
192    pub fn scale(&self) -> f64 {
193        (self.kx + self.ky) * 0.5
194    }
195
196    /// Convert to an equivalent `TransAffine` matrix.
197    pub fn to_affine(&self) -> TransAffine {
198        let mut mtx = TransAffine::new_translation(-self.wx1, -self.wy1);
199        mtx.multiply(&TransAffine::new_scaling(self.kx, self.ky));
200        mtx.multiply(&TransAffine::new_translation(self.dx1, self.dy1));
201        mtx
202    }
203
204    /// Convert to an affine matrix with only the scale component.
205    pub fn to_affine_scale_only(&self) -> TransAffine {
206        TransAffine::new_scaling(self.kx, self.ky)
207    }
208
209    fn update(&mut self) {
210        const EPSILON: f64 = 1e-30;
211        if (self.world_x1 - self.world_x2).abs() < EPSILON
212            || (self.world_y1 - self.world_y2).abs() < EPSILON
213            || (self.device_x1 - self.device_x2).abs() < EPSILON
214            || (self.device_y1 - self.device_y2).abs() < EPSILON
215        {
216            self.wx1 = self.world_x1;
217            self.wy1 = self.world_y1;
218            self.wx2 = self.world_x1 + 1.0;
219            self.wy2 = self.world_y2 + 1.0;
220            self.dx1 = self.device_x1;
221            self.dy1 = self.device_y1;
222            self.kx = 1.0;
223            self.ky = 1.0;
224            self.is_valid = false;
225            return;
226        }
227
228        let mut world_x1 = self.world_x1;
229        let mut world_y1 = self.world_y1;
230        let mut world_x2 = self.world_x2;
231        let mut world_y2 = self.world_y2;
232        let device_x1 = self.device_x1;
233        let device_y1 = self.device_y1;
234        let device_x2 = self.device_x2;
235        let device_y2 = self.device_y2;
236
237        if self.aspect != AspectRatio::Stretch {
238            self.kx = (device_x2 - device_x1) / (world_x2 - world_x1);
239            self.ky = (device_y2 - device_y1) / (world_y2 - world_y1);
240
241            if (self.aspect == AspectRatio::Meet) == (self.kx < self.ky) {
242                let d = (world_y2 - world_y1) * self.ky / self.kx;
243                world_y1 += (world_y2 - world_y1 - d) * self.align_y;
244                world_y2 = world_y1 + d;
245            } else {
246                let d = (world_x2 - world_x1) * self.kx / self.ky;
247                world_x1 += (world_x2 - world_x1 - d) * self.align_x;
248                world_x2 = world_x1 + d;
249            }
250        }
251
252        self.wx1 = world_x1;
253        self.wy1 = world_y1;
254        self.wx2 = world_x2;
255        self.wy2 = world_y2;
256        self.dx1 = device_x1;
257        self.dy1 = device_y1;
258        self.kx = (device_x2 - device_x1) / (world_x2 - world_x1);
259        self.ky = (device_y2 - device_y1) / (world_y2 - world_y1);
260        self.is_valid = true;
261    }
262}
263
264// ============================================================================
265// Tests
266// ============================================================================
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_default_identity() {
274        let vp = TransViewport::new();
275        assert!(vp.is_valid());
276        assert_eq!(vp.scale_x(), 1.0);
277        assert_eq!(vp.scale_y(), 1.0);
278    }
279
280    #[test]
281    fn test_stretch_scaling() {
282        let mut vp = TransViewport::new();
283        vp.set_world_viewport(0.0, 0.0, 100.0, 100.0);
284        vp.set_device_viewport(0.0, 0.0, 200.0, 400.0);
285        assert_eq!(vp.scale_x(), 2.0);
286        assert_eq!(vp.scale_y(), 4.0);
287    }
288
289    #[test]
290    fn test_transform() {
291        let mut vp = TransViewport::new();
292        vp.set_world_viewport(0.0, 0.0, 100.0, 100.0);
293        vp.set_device_viewport(0.0, 0.0, 200.0, 200.0);
294
295        let mut x = 50.0;
296        let mut y = 50.0;
297        vp.transform(&mut x, &mut y);
298        assert_eq!(x, 100.0);
299        assert_eq!(y, 100.0);
300    }
301
302    #[test]
303    fn test_inverse_transform() {
304        let mut vp = TransViewport::new();
305        vp.set_world_viewport(0.0, 0.0, 100.0, 100.0);
306        vp.set_device_viewport(0.0, 0.0, 200.0, 200.0);
307
308        let mut x = 100.0;
309        let mut y = 100.0;
310        vp.inverse_transform(&mut x, &mut y);
311        assert_eq!(x, 50.0);
312        assert_eq!(y, 50.0);
313    }
314
315    #[test]
316    fn test_meet_aspect_ratio() {
317        let mut vp = TransViewport::new();
318        vp.set_world_viewport(0.0, 0.0, 100.0, 100.0);
319        vp.set_device_viewport(0.0, 0.0, 200.0, 400.0);
320        vp.preserve_aspect_ratio(0.5, 0.5, AspectRatio::Meet);
321        // Meet: use smaller scale factor (kx=2.0 < ky=4.0), so kx wins
322        assert_eq!(vp.scale_x(), 2.0);
323        assert_eq!(vp.scale_y(), 2.0);
324    }
325
326    #[test]
327    fn test_slice_aspect_ratio() {
328        let mut vp = TransViewport::new();
329        vp.set_world_viewport(0.0, 0.0, 100.0, 100.0);
330        vp.set_device_viewport(0.0, 0.0, 200.0, 400.0);
331        vp.preserve_aspect_ratio(0.5, 0.5, AspectRatio::Slice);
332        // Slice: use larger scale factor (ky=4.0 > kx=2.0), so ky wins
333        assert_eq!(vp.scale_x(), 4.0);
334        assert_eq!(vp.scale_y(), 4.0);
335    }
336
337    #[test]
338    fn test_to_affine() {
339        let mut vp = TransViewport::new();
340        vp.set_world_viewport(0.0, 0.0, 100.0, 100.0);
341        vp.set_device_viewport(0.0, 0.0, 200.0, 200.0);
342
343        let mtx = vp.to_affine();
344        let mut x = 50.0;
345        let mut y = 50.0;
346        mtx.transform(&mut x, &mut y);
347        assert!((x - 100.0).abs() < 1e-10);
348        assert!((y - 100.0).abs() < 1e-10);
349    }
350
351    #[test]
352    fn test_invalid_zero_size() {
353        let mut vp = TransViewport::new();
354        vp.set_world_viewport(50.0, 50.0, 50.0, 50.0); // zero-size world
355        assert!(!vp.is_valid());
356    }
357
358    #[test]
359    fn test_device_dx_dy() {
360        let mut vp = TransViewport::new();
361        vp.set_world_viewport(10.0, 20.0, 110.0, 120.0);
362        vp.set_device_viewport(0.0, 0.0, 200.0, 200.0);
363        // kx = 200/100 = 2, dx1 = 0, wx1 = 10
364        // device_dx = dx1 - wx1 * kx = 0 - 10 * 2 = -20
365        assert_eq!(vp.device_dx(), -20.0);
366    }
367
368    #[test]
369    fn test_scale() {
370        let mut vp = TransViewport::new();
371        vp.set_world_viewport(0.0, 0.0, 100.0, 100.0);
372        vp.set_device_viewport(0.0, 0.0, 200.0, 400.0);
373        assert_eq!(vp.scale(), 3.0); // (2 + 4) / 2
374    }
375}