Skip to main content

agg_rust/
bezier_arc.rs

1//! Bezier arc generator.
2//!
3//! Port of `agg_bezier_arc.h` / `agg_bezier_arc.cpp` — converts elliptical
4//! arcs into sequences of cubic Bezier curves. Produces at most 4 consecutive
5//! cubic Bezier curves (4, 7, 10, or 13 vertices).
6
7use crate::basics::{
8    VertexSource, PATH_CMD_CURVE4, PATH_CMD_LINE_TO, PATH_CMD_MOVE_TO, PATH_CMD_STOP, PI,
9};
10use crate::trans_affine::TransAffine;
11
12/// Epsilon to prevent adding degenerate curves.
13const BEZIER_ARC_ANGLE_EPSILON: f64 = 0.01;
14
15/// Convert an arc segment to a single cubic Bezier curve (4 control points).
16///
17/// Writes 8 values to `curve`: `[x0, y0, x1, y1, x2, y2, x3, y3]`.
18pub fn arc_to_bezier(
19    cx: f64,
20    cy: f64,
21    rx: f64,
22    ry: f64,
23    start_angle: f64,
24    sweep_angle: f64,
25    curve: &mut [f64],
26) {
27    let x0 = (sweep_angle / 2.0).cos();
28    let y0 = (sweep_angle / 2.0).sin();
29    let tx = (1.0 - x0) * 4.0 / 3.0;
30    let ty = y0 - tx * x0 / y0;
31
32    let px = [x0, x0 + tx, x0 + tx, x0];
33    let py = [-y0, -ty, ty, y0];
34
35    let sn = (start_angle + sweep_angle / 2.0).sin();
36    let cs = (start_angle + sweep_angle / 2.0).cos();
37
38    for i in 0..4 {
39        curve[i * 2] = cx + rx * (px[i] * cs - py[i] * sn);
40        curve[i * 2 + 1] = cy + ry * (px[i] * sn + py[i] * cs);
41    }
42}
43
44/// Bezier arc generator.
45///
46/// Generates up to 4 consecutive cubic Bezier curves from an elliptical arc.
47///
48/// Port of C++ `agg::bezier_arc`.
49pub struct BezierArc {
50    vertex: usize,
51    num_vertices: usize,
52    vertices: [f64; 26],
53    cmd: u32,
54}
55
56impl BezierArc {
57    /// Create an uninitialized bezier arc.
58    pub fn new() -> Self {
59        Self {
60            vertex: 26,
61            num_vertices: 0,
62            vertices: [0.0; 26],
63            cmd: PATH_CMD_LINE_TO,
64        }
65    }
66
67    /// Create and initialize a bezier arc.
68    pub fn new_with_params(
69        x: f64,
70        y: f64,
71        rx: f64,
72        ry: f64,
73        start_angle: f64,
74        sweep_angle: f64,
75    ) -> Self {
76        let mut arc = Self::new();
77        arc.init(x, y, rx, ry, start_angle, sweep_angle);
78        arc
79    }
80
81    /// Initialize the arc with center, radii, and angle parameters.
82    pub fn init(&mut self, x: f64, y: f64, rx: f64, ry: f64, start_angle: f64, sweep_angle: f64) {
83        let mut start_angle = start_angle % (2.0 * PI);
84        let mut sweep_angle = sweep_angle;
85
86        if sweep_angle >= 2.0 * PI {
87            sweep_angle = 2.0 * PI;
88        }
89        if sweep_angle <= -2.0 * PI {
90            sweep_angle = -2.0 * PI;
91        }
92
93        if sweep_angle.abs() < 1e-10 {
94            self.num_vertices = 4;
95            self.cmd = PATH_CMD_LINE_TO;
96            self.vertices[0] = x + rx * start_angle.cos();
97            self.vertices[1] = y + ry * start_angle.sin();
98            self.vertices[2] = x + rx * (start_angle + sweep_angle).cos();
99            self.vertices[3] = y + ry * (start_angle + sweep_angle).sin();
100            return;
101        }
102
103        let mut total_sweep = 0.0;
104        let mut local_sweep;
105        self.num_vertices = 2;
106        self.cmd = PATH_CMD_CURVE4;
107        let mut done = false;
108
109        loop {
110            if sweep_angle < 0.0 {
111                let prev_sweep = total_sweep;
112                local_sweep = -PI * 0.5;
113                total_sweep -= PI * 0.5;
114                if total_sweep <= sweep_angle + BEZIER_ARC_ANGLE_EPSILON {
115                    local_sweep = sweep_angle - prev_sweep;
116                    done = true;
117                }
118            } else {
119                let prev_sweep = total_sweep;
120                local_sweep = PI * 0.5;
121                total_sweep += PI * 0.5;
122                if total_sweep >= sweep_angle - BEZIER_ARC_ANGLE_EPSILON {
123                    local_sweep = sweep_angle - prev_sweep;
124                    done = true;
125                }
126            }
127
128            arc_to_bezier(
129                x,
130                y,
131                rx,
132                ry,
133                start_angle,
134                local_sweep,
135                &mut self.vertices[self.num_vertices - 2..],
136            );
137
138            self.num_vertices += 6;
139            start_angle += local_sweep;
140
141            if done || self.num_vertices >= 26 {
142                break;
143            }
144        }
145    }
146
147    /// Number of coordinate values (doubled number of vertices).
148    pub fn num_vertices(&self) -> usize {
149        self.num_vertices
150    }
151
152    /// Access the vertex coordinate array.
153    pub fn vertices(&self) -> &[f64; 26] {
154        &self.vertices
155    }
156
157    /// Mutable access to the vertex coordinate array.
158    pub fn vertices_mut(&mut self) -> &mut [f64; 26] {
159        &mut self.vertices
160    }
161}
162
163impl Default for BezierArc {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169impl VertexSource for BezierArc {
170    fn rewind(&mut self, _path_id: u32) {
171        self.vertex = 0;
172    }
173
174    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
175        if self.vertex >= self.num_vertices {
176            return PATH_CMD_STOP;
177        }
178        *x = self.vertices[self.vertex];
179        *y = self.vertices[self.vertex + 1];
180        self.vertex += 2;
181        if self.vertex == 2 {
182            PATH_CMD_MOVE_TO
183        } else {
184            self.cmd
185        }
186    }
187}
188
189/// SVG-style bezier arc generator.
190///
191/// Computes an elliptical arc from `(x1, y1)` to `(x2, y2)` using SVG
192/// endpoint parameterization (radii, rotation, flags).
193///
194/// Port of C++ `agg::bezier_arc_svg`.
195pub struct BezierArcSvg {
196    arc: BezierArc,
197    radii_ok: bool,
198}
199
200impl BezierArcSvg {
201    /// Create an uninitialized SVG bezier arc.
202    pub fn new() -> Self {
203        Self {
204            arc: BezierArc::new(),
205            radii_ok: false,
206        }
207    }
208
209    /// Create and initialize an SVG bezier arc.
210    #[allow(clippy::too_many_arguments)]
211    pub fn new_with_params(
212        x1: f64,
213        y1: f64,
214        rx: f64,
215        ry: f64,
216        angle: f64,
217        large_arc_flag: bool,
218        sweep_flag: bool,
219        x2: f64,
220        y2: f64,
221    ) -> Self {
222        let mut svg = Self::new();
223        svg.init(x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2);
224        svg
225    }
226
227    /// Initialize with SVG arc parameters.
228    #[allow(clippy::too_many_arguments)]
229    pub fn init(
230        &mut self,
231        x0: f64,
232        y0: f64,
233        rx: f64,
234        ry: f64,
235        angle: f64,
236        large_arc_flag: bool,
237        sweep_flag: bool,
238        x2: f64,
239        y2: f64,
240    ) {
241        self.radii_ok = true;
242
243        let mut rx = rx.abs();
244        let mut ry = ry.abs();
245
246        // Calculate midpoint
247        let dx2 = (x0 - x2) / 2.0;
248        let dy2 = (y0 - y2) / 2.0;
249
250        let cos_a = angle.cos();
251        let sin_a = angle.sin();
252
253        // Rotate to align with axes
254        let x1 = cos_a * dx2 + sin_a * dy2;
255        let y1 = -sin_a * dx2 + cos_a * dy2;
256
257        // Ensure radii are large enough
258        let mut prx = rx * rx;
259        let mut pry = ry * ry;
260        let px1 = x1 * x1;
261        let py1 = y1 * y1;
262
263        let radii_check = px1 / prx + py1 / pry;
264        if radii_check > 1.0 {
265            rx *= radii_check.sqrt();
266            ry *= radii_check.sqrt();
267            prx = rx * rx;
268            pry = ry * ry;
269            if radii_check > 10.0 {
270                self.radii_ok = false;
271            }
272        }
273
274        // Calculate center
275        let sign = if large_arc_flag == sweep_flag {
276            -1.0
277        } else {
278            1.0
279        };
280        let sq = (prx * pry - prx * py1 - pry * px1) / (prx * py1 + pry * px1);
281        let coef = sign * (if sq < 0.0 { 0.0 } else { sq }).sqrt();
282        let cx1 = coef * ((rx * y1) / ry);
283        let cy1 = coef * -((ry * x1) / rx);
284
285        // Transform center back
286        let sx2 = (x0 + x2) / 2.0;
287        let sy2 = (y0 + y2) / 2.0;
288        let cx = sx2 + (cos_a * cx1 - sin_a * cy1);
289        let cy = sy2 + (sin_a * cx1 + cos_a * cy1);
290
291        // Calculate angles
292        let ux = (x1 - cx1) / rx;
293        let uy = (y1 - cy1) / ry;
294        let vx = (-x1 - cx1) / rx;
295        let vy = (-y1 - cy1) / ry;
296
297        // Start angle
298        let n = (ux * ux + uy * uy).sqrt();
299        let p = ux;
300        let sign = if uy < 0.0 { -1.0 } else { 1.0 };
301        let v = (p / n).clamp(-1.0, 1.0);
302        let start_angle = sign * v.acos();
303
304        // Sweep angle
305        let n = ((ux * ux + uy * uy) * (vx * vx + vy * vy)).sqrt();
306        let p = ux * vx + uy * vy;
307        let sign = if ux * vy - uy * vx < 0.0 { -1.0 } else { 1.0 };
308        let v = (p / n).clamp(-1.0, 1.0);
309        let mut sweep_angle = sign * v.acos();
310
311        if !sweep_flag && sweep_angle > 0.0 {
312            sweep_angle -= PI * 2.0;
313        } else if sweep_flag && sweep_angle < 0.0 {
314            sweep_angle += PI * 2.0;
315        }
316
317        // Build and transform the arc
318        self.arc.init(0.0, 0.0, rx, ry, start_angle, sweep_angle);
319
320        let mut mtx = TransAffine::new_rotation(angle);
321        mtx.multiply(&TransAffine::new_translation(cx, cy));
322
323        let nv = self.arc.num_vertices();
324        if nv > 4 {
325            let verts = self.arc.vertices_mut();
326            for i in (2..nv - 2).step_by(2) {
327                let mut tx = verts[i];
328                let mut ty = verts[i + 1];
329                mtx.transform(&mut tx, &mut ty);
330                verts[i] = tx;
331                verts[i + 1] = ty;
332            }
333        }
334
335        // Ensure exact start and end points
336        let verts = self.arc.vertices_mut();
337        verts[0] = x0;
338        verts[1] = y0;
339        if nv > 2 {
340            verts[nv - 2] = x2;
341            verts[nv - 1] = y2;
342        }
343    }
344
345    /// Whether the radii were sufficient (not enlarged).
346    pub fn radii_ok(&self) -> bool {
347        self.radii_ok
348    }
349
350    /// Number of coordinate values.
351    pub fn num_vertices(&self) -> usize {
352        self.arc.num_vertices()
353    }
354
355    /// Access the vertex coordinate array.
356    pub fn vertices(&self) -> &[f64; 26] {
357        self.arc.vertices()
358    }
359}
360
361impl Default for BezierArcSvg {
362    fn default() -> Self {
363        Self::new()
364    }
365}
366
367impl VertexSource for BezierArcSvg {
368    fn rewind(&mut self, _path_id: u32) {
369        self.arc.rewind(0);
370    }
371
372    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
373        self.arc.vertex(x, y)
374    }
375}
376
377// ============================================================================
378// Tests
379// ============================================================================
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384    use crate::basics::is_stop;
385
386    #[test]
387    fn test_bezier_arc_quarter() {
388        let mut arc = BezierArc::new_with_params(0.0, 0.0, 10.0, 10.0, 0.0, PI / 2.0);
389        arc.rewind(0);
390        let mut x = 0.0;
391        let mut y = 0.0;
392
393        // Should start with move_to
394        let cmd = arc.vertex(&mut x, &mut y);
395        assert_eq!(cmd, PATH_CMD_MOVE_TO);
396        assert!((x - 10.0).abs() < 1e-6);
397        assert!(y.abs() < 1e-6);
398
399        // Next 3 vertices are curve4 control points
400        for _ in 0..3 {
401            let cmd = arc.vertex(&mut x, &mut y);
402            assert_eq!(cmd, PATH_CMD_CURVE4);
403        }
404
405        // Should stop
406        let cmd = arc.vertex(&mut x, &mut y);
407        assert!(is_stop(cmd));
408    }
409
410    #[test]
411    fn test_bezier_arc_full_circle() {
412        let arc = BezierArc::new_with_params(0.0, 0.0, 10.0, 10.0, 0.0, 2.0 * PI);
413        // Full circle: 4 quadrants × 3 control points + 1 start + 1 end = 14 values
414        // Actually: num_vertices = 2 + 4*6 = 26
415        assert_eq!(arc.num_vertices(), 26);
416    }
417
418    #[test]
419    fn test_bezier_arc_half_circle() {
420        let arc = BezierArc::new_with_params(0.0, 0.0, 10.0, 10.0, 0.0, PI);
421        // Half circle: 2 quadrants × 6 + 2 = 14
422        assert_eq!(arc.num_vertices(), 14);
423    }
424
425    #[test]
426    fn test_bezier_arc_tiny_sweep() {
427        let arc = BezierArc::new_with_params(0.0, 0.0, 10.0, 10.0, 0.0, 1e-15);
428        // Tiny sweep: degenerate case, just a line
429        assert_eq!(arc.num_vertices(), 4);
430        assert_eq!(arc.cmd, PATH_CMD_LINE_TO);
431    }
432
433    #[test]
434    fn test_bezier_arc_negative_sweep() {
435        let arc = BezierArc::new_with_params(0.0, 0.0, 10.0, 10.0, 0.0, -PI / 2.0);
436        // Negative quarter arc
437        assert_eq!(arc.num_vertices(), 8);
438    }
439
440    #[test]
441    fn test_bezier_arc_svg_basic() {
442        let mut svg = BezierArcSvg::new_with_params(
443            0.0, 10.0, // start
444            10.0, 10.0, // radii
445            0.0,  // angle
446            false, true, // flags
447            10.0, 0.0, // end
448        );
449        assert!(svg.radii_ok());
450
451        svg.rewind(0);
452        let mut x = 0.0;
453        let mut y = 0.0;
454
455        // First vertex should be the start point
456        let cmd = svg.vertex(&mut x, &mut y);
457        assert_eq!(cmd, PATH_CMD_MOVE_TO);
458        assert!((x - 0.0).abs() < 1e-6);
459        assert!((y - 10.0).abs() < 1e-6);
460
461        // Consume remaining vertices
462        let mut count = 1;
463        while !is_stop(svg.vertex(&mut x, &mut y)) {
464            count += 1;
465        }
466        assert!(count >= 4);
467
468        // Last vertex should be near the end point
469        assert!((x - 10.0).abs() < 1e-6);
470        assert!((y - 0.0).abs() < 1e-6);
471    }
472
473    #[test]
474    fn test_arc_to_bezier_endpoints() {
475        let mut curve = [0.0; 8];
476        arc_to_bezier(0.0, 0.0, 10.0, 10.0, 0.0, PI / 2.0, &mut curve);
477
478        // Start point should be at angle 0 → (10, 0)
479        let start_x = curve[0];
480        let start_y = curve[1];
481        assert!((start_x - 10.0).abs() < 1e-6);
482        assert!(start_y.abs() < 1e-6);
483
484        // End point should be at angle π/2 → (0, 10)
485        let end_x = curve[6];
486        let end_y = curve[7];
487        assert!(end_x.abs() < 1e-6);
488        assert!((end_y - 10.0).abs() < 1e-6);
489    }
490
491    #[test]
492    fn test_bezier_arc_default() {
493        let arc = BezierArc::new();
494        assert_eq!(arc.num_vertices(), 0);
495    }
496
497    #[test]
498    fn test_bezier_arc_svg_small_radii() {
499        // Radii too small - should be enlarged
500        let svg = BezierArcSvg::new_with_params(
501            0.0, 0.0, // start
502            1.0, 1.0, // tiny radii
503            0.0, // angle
504            false, true, // flags
505            100.0, 100.0, // end far away
506        );
507        // Radii were enlarged significantly
508        assert!(!svg.radii_ok());
509    }
510}