Skip to main content

agg_rust/
rounded_rect.rs

1//! Rounded rectangle vertex generator.
2//!
3//! Port of `agg_rounded_rect.h` / `agg_rounded_rect.cpp` — generates vertices
4//! for a rectangle with independently controllable corner radii.
5
6use crate::arc::Arc;
7use crate::basics::{
8    is_stop, VertexSource, PATH_CMD_END_POLY, PATH_CMD_LINE_TO, PATH_CMD_STOP, PATH_FLAGS_CCW,
9    PATH_FLAGS_CLOSE, PI,
10};
11
12/// Rounded rectangle vertex source.
13///
14/// Generates vertices for a rectangle where each corner can have its own
15/// elliptical radius. The corners are emitted as arcs (bottom-left, bottom-right,
16/// top-right, top-left) connected by line segments, forming a closed polygon.
17///
18/// Port of C++ `agg::rounded_rect`.
19pub struct RoundedRect {
20    x1: f64,
21    y1: f64,
22    x2: f64,
23    y2: f64,
24    rx1: f64,
25    ry1: f64,
26    rx2: f64,
27    ry2: f64,
28    rx3: f64,
29    ry3: f64,
30    rx4: f64,
31    ry4: f64,
32    status: u32,
33    arc: Arc,
34}
35
36impl RoundedRect {
37    /// Create a new rounded rectangle with uniform corner radius.
38    pub fn new(x1: f64, y1: f64, x2: f64, y2: f64, r: f64) -> Self {
39        let (x1, x2) = if x1 > x2 { (x2, x1) } else { (x1, x2) };
40        let (y1, y2) = if y1 > y2 { (y2, y1) } else { (y1, y2) };
41        Self {
42            x1,
43            y1,
44            x2,
45            y2,
46            rx1: r,
47            ry1: r,
48            rx2: r,
49            ry2: r,
50            rx3: r,
51            ry3: r,
52            rx4: r,
53            ry4: r,
54            status: 0,
55            arc: Arc::default_new(),
56        }
57    }
58
59    /// Create a default (zero-sized) rounded rectangle.
60    pub fn default_new() -> Self {
61        Self {
62            x1: 0.0,
63            y1: 0.0,
64            x2: 0.0,
65            y2: 0.0,
66            rx1: 0.0,
67            ry1: 0.0,
68            rx2: 0.0,
69            ry2: 0.0,
70            rx3: 0.0,
71            ry3: 0.0,
72            rx4: 0.0,
73            ry4: 0.0,
74            status: 0,
75            arc: Arc::default_new(),
76        }
77    }
78
79    /// Set the rectangle coordinates.
80    pub fn rect(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
81        self.x1 = x1;
82        self.y1 = y1;
83        self.x2 = x2;
84        self.y2 = y2;
85        if x1 > x2 {
86            self.x1 = x2;
87            self.x2 = x1;
88        }
89        if y1 > y2 {
90            self.y1 = y2;
91            self.y2 = y1;
92        }
93    }
94
95    /// Set uniform corner radius.
96    pub fn radius(&mut self, r: f64) {
97        self.rx1 = r;
98        self.ry1 = r;
99        self.rx2 = r;
100        self.ry2 = r;
101        self.rx3 = r;
102        self.ry3 = r;
103        self.rx4 = r;
104        self.ry4 = r;
105    }
106
107    /// Set corner radii with separate x/y values (uniform across corners).
108    pub fn radius_xy(&mut self, rx: f64, ry: f64) {
109        self.rx1 = rx;
110        self.rx2 = rx;
111        self.rx3 = rx;
112        self.rx4 = rx;
113        self.ry1 = ry;
114        self.ry2 = ry;
115        self.ry3 = ry;
116        self.ry4 = ry;
117    }
118
119    /// Set corner radii for bottom and top edges.
120    pub fn radius_bottom_top(&mut self, rx_bottom: f64, ry_bottom: f64, rx_top: f64, ry_top: f64) {
121        self.rx1 = rx_bottom;
122        self.rx2 = rx_bottom;
123        self.rx3 = rx_top;
124        self.rx4 = rx_top;
125        self.ry1 = ry_bottom;
126        self.ry2 = ry_bottom;
127        self.ry3 = ry_top;
128        self.ry4 = ry_top;
129    }
130
131    /// Set each corner radius individually.
132    ///
133    /// Corners are numbered 1-4 starting from bottom-left going clockwise:
134    /// 1 = bottom-left, 2 = bottom-right, 3 = top-right, 4 = top-left.
135    #[allow(clippy::too_many_arguments)]
136    pub fn radius_all(
137        &mut self,
138        rx1: f64,
139        ry1: f64,
140        rx2: f64,
141        ry2: f64,
142        rx3: f64,
143        ry3: f64,
144        rx4: f64,
145        ry4: f64,
146    ) {
147        self.rx1 = rx1;
148        self.ry1 = ry1;
149        self.rx2 = rx2;
150        self.ry2 = ry2;
151        self.rx3 = rx3;
152        self.ry3 = ry3;
153        self.rx4 = rx4;
154        self.ry4 = ry4;
155    }
156
157    /// Normalize radii so they don't exceed rectangle dimensions.
158    ///
159    /// If the sum of adjacent corner radii exceeds the corresponding
160    /// dimension, all radii are uniformly scaled down.
161    pub fn normalize_radius(&mut self) {
162        let dx = (self.y2 - self.y1).abs();
163        let dy = (self.x2 - self.x1).abs();
164
165        let mut k = 1.0_f64;
166        let t = dx / (self.rx1 + self.rx2);
167        if t < k {
168            k = t;
169        }
170        let t = dx / (self.rx3 + self.rx4);
171        if t < k {
172            k = t;
173        }
174        let t = dy / (self.ry1 + self.ry2);
175        if t < k {
176            k = t;
177        }
178        let t = dy / (self.ry3 + self.ry4);
179        if t < k {
180            k = t;
181        }
182
183        if k < 1.0 {
184            self.rx1 *= k;
185            self.ry1 *= k;
186            self.rx2 *= k;
187            self.ry2 *= k;
188            self.rx3 *= k;
189            self.ry3 *= k;
190            self.rx4 *= k;
191            self.ry4 *= k;
192        }
193    }
194
195    /// Set the approximation scale for arc generation.
196    pub fn set_approximation_scale(&mut self, s: f64) {
197        self.arc.set_approximation_scale(s);
198    }
199
200    /// Get the current approximation scale.
201    pub fn approximation_scale(&self) -> f64 {
202        self.arc.approximation_scale()
203    }
204}
205
206impl VertexSource for RoundedRect {
207    fn rewind(&mut self, _path_id: u32) {
208        self.status = 0;
209    }
210
211    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
212        let mut cmd;
213        loop {
214            match self.status {
215                0 => {
216                    // Bottom-left corner arc (PI to 3PI/2)
217                    self.arc.init(
218                        self.x1 + self.rx1,
219                        self.y1 + self.ry1,
220                        self.rx1,
221                        self.ry1,
222                        PI,
223                        PI + PI * 0.5,
224                        true,
225                    );
226                    self.arc.rewind(0);
227                    self.status += 1;
228                }
229                1 => {
230                    cmd = self.arc.vertex(x, y);
231                    if is_stop(cmd) {
232                        self.status += 1;
233                    } else {
234                        return cmd;
235                    }
236                }
237                2 => {
238                    // Bottom-right corner arc (3PI/2 to 2PI)
239                    self.arc.init(
240                        self.x2 - self.rx2,
241                        self.y1 + self.ry2,
242                        self.rx2,
243                        self.ry2,
244                        PI + PI * 0.5,
245                        0.0,
246                        true,
247                    );
248                    self.arc.rewind(0);
249                    self.status += 1;
250                }
251                3 => {
252                    cmd = self.arc.vertex(x, y);
253                    if is_stop(cmd) {
254                        self.status += 1;
255                    } else {
256                        return PATH_CMD_LINE_TO;
257                    }
258                }
259                4 => {
260                    // Top-right corner arc (0 to PI/2)
261                    self.arc.init(
262                        self.x2 - self.rx3,
263                        self.y2 - self.ry3,
264                        self.rx3,
265                        self.ry3,
266                        0.0,
267                        PI * 0.5,
268                        true,
269                    );
270                    self.arc.rewind(0);
271                    self.status += 1;
272                }
273                5 => {
274                    cmd = self.arc.vertex(x, y);
275                    if is_stop(cmd) {
276                        self.status += 1;
277                    } else {
278                        return PATH_CMD_LINE_TO;
279                    }
280                }
281                6 => {
282                    // Top-left corner arc (PI/2 to PI)
283                    self.arc.init(
284                        self.x1 + self.rx4,
285                        self.y2 - self.ry4,
286                        self.rx4,
287                        self.ry4,
288                        PI * 0.5,
289                        PI,
290                        true,
291                    );
292                    self.arc.rewind(0);
293                    self.status += 1;
294                }
295                7 => {
296                    cmd = self.arc.vertex(x, y);
297                    if is_stop(cmd) {
298                        self.status += 1;
299                    } else {
300                        return PATH_CMD_LINE_TO;
301                    }
302                }
303                8 => {
304                    cmd = PATH_CMD_END_POLY | PATH_FLAGS_CLOSE | PATH_FLAGS_CCW;
305                    self.status += 1;
306                    return cmd;
307                }
308                _ => {
309                    return PATH_CMD_STOP;
310                }
311            }
312        }
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use crate::basics::{is_close, is_end_poly, is_move_to, is_vertex, PATH_CMD_MOVE_TO};
320
321    #[test]
322    fn test_new_normalizes_coords() {
323        let rr = RoundedRect::new(100.0, 200.0, 50.0, 30.0, 5.0);
324        assert_eq!(rr.x1, 50.0);
325        assert_eq!(rr.y1, 30.0);
326        assert_eq!(rr.x2, 100.0);
327        assert_eq!(rr.y2, 200.0);
328    }
329
330    #[test]
331    fn test_uniform_radius() {
332        let mut rr = RoundedRect::default_new();
333        rr.radius(10.0);
334        assert_eq!(rr.rx1, 10.0);
335        assert_eq!(rr.ry1, 10.0);
336        assert_eq!(rr.rx2, 10.0);
337        assert_eq!(rr.ry2, 10.0);
338        assert_eq!(rr.rx3, 10.0);
339        assert_eq!(rr.ry3, 10.0);
340        assert_eq!(rr.rx4, 10.0);
341        assert_eq!(rr.ry4, 10.0);
342    }
343
344    #[test]
345    fn test_radius_xy() {
346        let mut rr = RoundedRect::default_new();
347        rr.radius_xy(10.0, 5.0);
348        assert_eq!(rr.rx1, 10.0);
349        assert_eq!(rr.ry1, 5.0);
350        assert_eq!(rr.rx2, 10.0);
351        assert_eq!(rr.ry2, 5.0);
352    }
353
354    #[test]
355    fn test_radius_bottom_top() {
356        let mut rr = RoundedRect::default_new();
357        rr.radius_bottom_top(3.0, 4.0, 5.0, 6.0);
358        assert_eq!(rr.rx1, 3.0);
359        assert_eq!(rr.ry1, 4.0);
360        assert_eq!(rr.rx2, 3.0);
361        assert_eq!(rr.ry2, 4.0);
362        assert_eq!(rr.rx3, 5.0);
363        assert_eq!(rr.ry3, 6.0);
364        assert_eq!(rr.rx4, 5.0);
365        assert_eq!(rr.ry4, 6.0);
366    }
367
368    #[test]
369    fn test_radius_all() {
370        let mut rr = RoundedRect::default_new();
371        rr.radius_all(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0);
372        assert_eq!(rr.rx1, 1.0);
373        assert_eq!(rr.ry1, 2.0);
374        assert_eq!(rr.rx2, 3.0);
375        assert_eq!(rr.ry2, 4.0);
376        assert_eq!(rr.rx3, 5.0);
377        assert_eq!(rr.ry3, 6.0);
378        assert_eq!(rr.rx4, 7.0);
379        assert_eq!(rr.ry4, 8.0);
380    }
381
382    #[test]
383    fn test_normalize_radius_no_change() {
384        // Radii small enough — no scaling needed
385        let mut rr = RoundedRect::new(0.0, 0.0, 100.0, 100.0, 5.0);
386        rr.normalize_radius();
387        assert!((rr.rx1 - 5.0).abs() < 1e-10);
388    }
389
390    #[test]
391    fn test_normalize_radius_scales_down() {
392        // Radii too large — must scale down
393        let mut rr = RoundedRect::new(0.0, 0.0, 20.0, 20.0, 15.0);
394        rr.normalize_radius();
395        // rx1 + rx2 = 30 > width 20, so k = 20/30 = 2/3
396        // Note: C++ uses dx = |y2-y1| and dy = |x2-x1| (swapped labels)
397        let expected = 15.0 * (20.0 / 30.0);
398        assert!((rr.rx1 - expected).abs() < 1e-10);
399    }
400
401    #[test]
402    fn test_vertex_generation_produces_closed_shape() {
403        let mut rr = RoundedRect::new(10.0, 10.0, 90.0, 90.0, 10.0);
404        rr.rewind(0);
405
406        let mut x = 0.0;
407        let mut y = 0.0;
408        let mut vertex_count = 0;
409        let mut has_move_to = false;
410        let mut has_end_poly = false;
411
412        loop {
413            let cmd = rr.vertex(&mut x, &mut y);
414            if is_stop(cmd) {
415                break;
416            }
417            if is_move_to(cmd) {
418                has_move_to = true;
419            }
420            if is_end_poly(cmd) {
421                has_end_poly = true;
422            }
423            if is_vertex(cmd) {
424                vertex_count += 1;
425            }
426        }
427
428        assert!(has_move_to, "Should start with move_to");
429        assert!(has_end_poly, "Should end with end_poly");
430        assert!(vertex_count > 4, "Should have more than 4 vertices (arcs)");
431    }
432
433    #[test]
434    fn test_end_poly_has_close_and_ccw_flags() {
435        let mut rr = RoundedRect::new(0.0, 0.0, 100.0, 100.0, 10.0);
436        rr.rewind(0);
437
438        let mut x = 0.0;
439        let mut y = 0.0;
440
441        loop {
442            let cmd = rr.vertex(&mut x, &mut y);
443            if is_stop(cmd) {
444                panic!("Should have end_poly before stop");
445            }
446            if is_end_poly(cmd) {
447                assert!(is_close(cmd), "end_poly should have close flag");
448                assert!((cmd & PATH_FLAGS_CCW) != 0, "Should have CCW flag");
449                break;
450            }
451        }
452    }
453
454    #[test]
455    fn test_first_vertex_is_on_bottom_left_arc() {
456        let mut rr = RoundedRect::new(10.0, 20.0, 90.0, 80.0, 10.0);
457        rr.rewind(0);
458
459        let mut x = 0.0;
460        let mut y = 0.0;
461        let cmd = rr.vertex(&mut x, &mut y);
462        assert_eq!(cmd, PATH_CMD_MOVE_TO);
463
464        // First vertex should be near the bottom-left corner.
465        // Arc center is (20, 30), radius 10, starting at PI.
466        // At angle PI: x = 20 + 10*cos(PI) = 10, y = 30 + 10*sin(PI) = 30
467        assert!((x - 10.0).abs() < 0.5, "x={x}, expected near 10");
468        assert!((y - 30.0).abs() < 0.5, "y={y}, expected near 30");
469    }
470
471    #[test]
472    fn test_approximation_scale() {
473        let mut rr = RoundedRect::new(0.0, 0.0, 100.0, 100.0, 10.0);
474        rr.set_approximation_scale(2.0);
475        assert!((rr.approximation_scale() - 2.0).abs() < 1e-10);
476    }
477
478    #[test]
479    fn test_rect_method() {
480        let mut rr = RoundedRect::default_new();
481        rr.rect(100.0, 200.0, 50.0, 30.0);
482        assert_eq!(rr.x1, 50.0);
483        assert_eq!(rr.y1, 30.0);
484        assert_eq!(rr.x2, 100.0);
485        assert_eq!(rr.y2, 200.0);
486    }
487
488    #[test]
489    fn test_zero_radius_produces_rectangle() {
490        let mut rr = RoundedRect::new(0.0, 0.0, 100.0, 100.0, 0.0);
491        rr.rewind(0);
492
493        let mut x = 0.0;
494        let mut y = 0.0;
495        let mut vertex_count = 0;
496
497        loop {
498            let cmd = rr.vertex(&mut x, &mut y);
499            if is_stop(cmd) {
500                break;
501            }
502            if is_vertex(cmd) {
503                vertex_count += 1;
504            }
505        }
506
507        // With zero radius, arcs degenerate — should still produce a valid shape
508        assert!(vertex_count >= 4);
509    }
510}