Skip to main content

agg_rust/
arc.rs

1//! Arc vertex generator.
2//!
3//! Port of `agg_arc.h` / `agg_arc.cpp` — generates vertices along an
4//! elliptical arc, suitable for use as a VertexSource in the rendering
5//! pipeline.
6
7use crate::basics::{is_stop, VertexSource, PATH_CMD_LINE_TO, PATH_CMD_MOVE_TO, PATH_CMD_STOP, PI};
8
9/// Arc vertex generator.
10///
11/// Generates vertices along an elliptical arc defined by center, radii,
12/// start angle, end angle, and direction (CW/CCW).
13///
14/// Port of C++ `agg::arc`.
15pub struct Arc {
16    x: f64,
17    y: f64,
18    rx: f64,
19    ry: f64,
20    angle: f64,
21    start: f64,
22    end: f64,
23    scale: f64,
24    da: f64,
25    ccw: bool,
26    initialized: bool,
27    path_cmd: u32,
28}
29
30impl Arc {
31    /// Create a new arc.
32    #[allow(clippy::too_many_arguments)]
33    pub fn new(x: f64, y: f64, rx: f64, ry: f64, a1: f64, a2: f64, ccw: bool) -> Self {
34        let mut arc = Self {
35            x,
36            y,
37            rx,
38            ry,
39            angle: 0.0,
40            start: 0.0,
41            end: 0.0,
42            scale: 1.0,
43            da: 0.0,
44            ccw: false,
45            initialized: false,
46            path_cmd: PATH_CMD_STOP,
47        };
48        arc.normalize(a1, a2, ccw);
49        arc
50    }
51
52    /// Create a default (uninitialized) arc.
53    pub fn default_new() -> Self {
54        Self {
55            x: 0.0,
56            y: 0.0,
57            rx: 0.0,
58            ry: 0.0,
59            angle: 0.0,
60            start: 0.0,
61            end: 0.0,
62            scale: 1.0,
63            da: 0.0,
64            ccw: false,
65            initialized: false,
66            path_cmd: PATH_CMD_STOP,
67        }
68    }
69
70    /// Re-initialize with new parameters.
71    #[allow(clippy::too_many_arguments)]
72    pub fn init(&mut self, x: f64, y: f64, rx: f64, ry: f64, a1: f64, a2: f64, ccw: bool) {
73        self.x = x;
74        self.y = y;
75        self.rx = rx;
76        self.ry = ry;
77        self.normalize(a1, a2, ccw);
78    }
79
80    /// Set approximation scale (affects step size).
81    pub fn set_approximation_scale(&mut self, s: f64) {
82        self.scale = s;
83        if self.initialized {
84            self.normalize(self.start, self.end, self.ccw);
85        }
86    }
87
88    /// Get current approximation scale.
89    pub fn approximation_scale(&self) -> f64 {
90        self.scale
91    }
92
93    /// Normalize angles and compute step size.
94    fn normalize(&mut self, a1: f64, a2: f64, ccw: bool) {
95        let ra = (self.rx.abs() + self.ry.abs()) / 2.0;
96        self.da = (ra / (ra + 0.125 / self.scale)).acos() * 2.0;
97
98        let mut a1 = a1;
99        let mut a2 = a2;
100
101        if ccw {
102            while a2 < a1 {
103                a2 += PI * 2.0;
104            }
105        } else {
106            while a1 < a2 {
107                a1 += PI * 2.0;
108            }
109            self.da = -self.da;
110        }
111
112        self.ccw = ccw;
113        self.start = a1;
114        self.end = a2;
115        self.initialized = true;
116    }
117}
118
119impl VertexSource for Arc {
120    fn rewind(&mut self, _path_id: u32) {
121        self.path_cmd = PATH_CMD_MOVE_TO;
122        self.angle = self.start;
123    }
124
125    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
126        if is_stop(self.path_cmd) {
127            return PATH_CMD_STOP;
128        }
129
130        if (self.angle < self.end - self.da / 4.0) != self.ccw {
131            *x = self.x + self.end.cos() * self.rx;
132            *y = self.y + self.end.sin() * self.ry;
133            self.path_cmd = PATH_CMD_STOP;
134            return PATH_CMD_LINE_TO;
135        }
136
137        *x = self.x + self.angle.cos() * self.rx;
138        *y = self.y + self.angle.sin() * self.ry;
139
140        self.angle += self.da;
141
142        let pf = self.path_cmd;
143        self.path_cmd = PATH_CMD_LINE_TO;
144        pf
145    }
146}
147
148// ============================================================================
149// Tests
150// ============================================================================
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn test_arc_full_circle_ccw() {
158        let mut arc = Arc::new(0.0, 0.0, 10.0, 10.0, 0.0, PI * 2.0, true);
159        arc.rewind(0);
160        let mut x = 0.0;
161        let mut y = 0.0;
162
163        // First vertex should be move_to
164        let cmd = arc.vertex(&mut x, &mut y);
165        assert_eq!(cmd, PATH_CMD_MOVE_TO);
166        assert!((x - 10.0).abs() < 1e-6);
167        assert!(y.abs() < 1e-6);
168
169        // Subsequent vertices should be line_to
170        let mut count = 1;
171        loop {
172            let cmd = arc.vertex(&mut x, &mut y);
173            if is_stop(cmd) {
174                break;
175            }
176            assert_eq!(cmd, PATH_CMD_LINE_TO);
177            count += 1;
178        }
179        // Full circle should generate multiple vertices
180        assert!(count > 4);
181    }
182
183    #[test]
184    fn test_arc_quarter_circle() {
185        let mut arc = Arc::new(0.0, 0.0, 10.0, 10.0, 0.0, PI / 2.0, true);
186        arc.rewind(0);
187        let mut x = 0.0;
188        let mut y = 0.0;
189
190        // First vertex at angle 0
191        let cmd = arc.vertex(&mut x, &mut y);
192        assert_eq!(cmd, PATH_CMD_MOVE_TO);
193        assert!((x - 10.0).abs() < 1e-6);
194        assert!(y.abs() < 1e-6);
195
196        // Collect last vertex (should be near angle PI/2)
197        let mut last_x = x;
198        let mut last_y = y;
199        loop {
200            let cmd = arc.vertex(&mut x, &mut y);
201            if is_stop(cmd) {
202                break;
203            }
204            last_x = x;
205            last_y = y;
206        }
207        // Last vertex should be at (0, 10) approximately
208        assert!(last_x.abs() < 1e-6);
209        assert!((last_y - 10.0).abs() < 1e-6);
210    }
211
212    #[test]
213    fn test_arc_cw_direction() {
214        let mut arc = Arc::new(0.0, 0.0, 10.0, 10.0, PI / 2.0, 0.0, false);
215        arc.rewind(0);
216        let mut x = 0.0;
217        let mut y = 0.0;
218
219        // First vertex should be at angle PI/2 (= 0, 10)
220        let cmd = arc.vertex(&mut x, &mut y);
221        assert_eq!(cmd, PATH_CMD_MOVE_TO);
222        assert!(x.abs() < 1e-6);
223        assert!((y - 10.0).abs() < 1e-6);
224
225        // Collect all vertices, last should be near (10, 0)
226        let mut last_x = x;
227        let mut last_y = y;
228        loop {
229            let cmd = arc.vertex(&mut x, &mut y);
230            if is_stop(cmd) {
231                break;
232            }
233            last_x = x;
234            last_y = y;
235        }
236        assert!((last_x - 10.0).abs() < 1e-6);
237        assert!(last_y.abs() < 1e-6);
238    }
239
240    #[test]
241    fn test_arc_elliptical() {
242        let mut arc = Arc::new(5.0, 5.0, 20.0, 10.0, 0.0, PI / 2.0, true);
243        arc.rewind(0);
244        let mut x = 0.0;
245        let mut y = 0.0;
246
247        // First vertex at center + (rx, 0)
248        let cmd = arc.vertex(&mut x, &mut y);
249        assert_eq!(cmd, PATH_CMD_MOVE_TO);
250        assert!((x - 25.0).abs() < 1e-6); // 5 + 20
251        assert!((y - 5.0).abs() < 1e-6);
252
253        // Last vertex at center + (0, ry)
254        let mut last_x = x;
255        let mut last_y = y;
256        loop {
257            let cmd = arc.vertex(&mut x, &mut y);
258            if is_stop(cmd) {
259                break;
260            }
261            last_x = x;
262            last_y = y;
263        }
264        assert!((last_x - 5.0).abs() < 1e-6); // center x
265        assert!((last_y - 15.0).abs() < 1e-6); // 5 + 10
266    }
267
268    #[test]
269    fn test_arc_rewind_restarts() {
270        let mut arc = Arc::new(0.0, 0.0, 10.0, 10.0, 0.0, PI, true);
271        let mut x = 0.0;
272        let mut y = 0.0;
273
274        // Consume all vertices
275        arc.rewind(0);
276        while !is_stop(arc.vertex(&mut x, &mut y)) {}
277
278        // After rewind, should start over
279        arc.rewind(0);
280        let cmd = arc.vertex(&mut x, &mut y);
281        assert_eq!(cmd, PATH_CMD_MOVE_TO);
282    }
283
284    #[test]
285    fn test_arc_approximation_scale() {
286        let mut arc = Arc::new(0.0, 0.0, 100.0, 100.0, 0.0, PI * 2.0, true);
287        arc.rewind(0);
288        let mut x = 0.0;
289        let mut y = 0.0;
290        let mut count1 = 0;
291        while !is_stop(arc.vertex(&mut x, &mut y)) {
292            count1 += 1;
293        }
294
295        // Higher scale = more vertices
296        arc.set_approximation_scale(4.0);
297        arc.rewind(0);
298        let mut count2 = 0;
299        while !is_stop(arc.vertex(&mut x, &mut y)) {
300            count2 += 1;
301        }
302        assert!(count2 > count1);
303    }
304
305    #[test]
306    fn test_arc_default_new() {
307        let arc = Arc::default_new();
308        assert_eq!(arc.approximation_scale(), 1.0);
309        assert!(!arc.initialized);
310    }
311}