kurbo/
arc.rs

1// Copyright 2019 the Kurbo Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! An ellipse arc.
5
6use crate::{Affine, Ellipse, PathEl, Point, Rect, Shape, Vec2};
7use core::{
8    f64::consts::{FRAC_PI_2, PI},
9    iter,
10    ops::Mul,
11};
12
13#[cfg(not(feature = "std"))]
14use crate::common::FloatFuncs;
15
16/// A single elliptical arc segment.
17#[derive(Clone, Copy, Debug, PartialEq)]
18#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20pub struct Arc {
21    /// The arc's centre point.
22    pub center: Point,
23    /// The arc's radii, where the vector's x-component is the radius in the
24    /// positive x direction after applying `x_rotation`.
25    pub radii: Vec2,
26    /// The start angle in radians.
27    pub start_angle: f64,
28    /// The angle between the start and end of the arc, in radians.
29    pub sweep_angle: f64,
30    /// How much the arc is rotated, in radians.
31    pub x_rotation: f64,
32}
33
34impl Arc {
35    /// Create a new `Arc`.
36    #[inline(always)]
37    pub fn new(
38        center: impl Into<Point>,
39        radii: impl Into<Vec2>,
40        start_angle: f64,
41        sweep_angle: f64,
42        x_rotation: f64,
43    ) -> Self {
44        Self {
45            center: center.into(),
46            radii: radii.into(),
47            start_angle,
48            sweep_angle,
49            x_rotation,
50        }
51    }
52
53    /// Returns a copy of this `Arc` in the opposite direction.
54    ///
55    /// The new `Arc` will sweep towards the original `Arc`s
56    /// start angle.
57    #[must_use]
58    #[inline]
59    pub fn reversed(&self) -> Arc {
60        Self {
61            center: self.center,
62            radii: self.radii,
63            start_angle: self.start_angle + self.sweep_angle,
64            sweep_angle: -self.sweep_angle,
65            x_rotation: self.x_rotation,
66        }
67    }
68
69    /// Create an iterator generating Bezier path elements.
70    ///
71    /// The generated elements can be appended to an existing bezier path.
72    pub fn append_iter(&self, tolerance: f64) -> ArcAppendIter {
73        let sign = self.sweep_angle.signum();
74        let scaled_err = self.radii.x.max(self.radii.y) / tolerance;
75        // Number of subdivisions per ellipse based on error tolerance.
76        // Note: this may slightly underestimate the error for quadrants.
77        let n_err = (1.1163 * scaled_err).powf(1.0 / 6.0).max(3.999_999);
78        let n = (n_err * self.sweep_angle.abs() * (1.0 / (2.0 * PI))).ceil();
79        let angle_step = self.sweep_angle / n;
80        let n = n as usize;
81        let arm_len = (4.0 / 3.0) * (0.25 * angle_step).abs().tan() * sign;
82        let angle0 = self.start_angle;
83        let p0 = sample_ellipse(self.radii, self.x_rotation, angle0);
84
85        ArcAppendIter {
86            idx: 0,
87
88            center: self.center,
89            radii: self.radii,
90            x_rotation: self.x_rotation,
91            n,
92            arm_len,
93            angle_step,
94
95            p0,
96            angle0,
97        }
98    }
99
100    /// Converts an `Arc` into a series of cubic bezier segments.
101    ///
102    /// The closure `p` will be invoked with the control points for each segment.
103    pub fn to_cubic_beziers<P>(self, tolerance: f64, mut p: P)
104    where
105        P: FnMut(Point, Point, Point),
106    {
107        let mut path = self.append_iter(tolerance);
108        while let Some(PathEl::CurveTo(p1, p2, p3)) = path.next() {
109            p(p1, p2, p3);
110        }
111    }
112}
113
114#[doc(hidden)]
115pub struct ArcAppendIter {
116    idx: usize,
117
118    center: Point,
119    radii: Vec2,
120    x_rotation: f64,
121    n: usize,
122    arm_len: f64,
123    angle_step: f64,
124
125    p0: Vec2,
126    angle0: f64,
127}
128
129impl Iterator for ArcAppendIter {
130    type Item = PathEl;
131
132    fn next(&mut self) -> Option<Self::Item> {
133        if self.idx >= self.n {
134            return None;
135        }
136
137        let angle1 = self.angle0 + self.angle_step;
138        let p0 = self.p0;
139        let p1 = p0
140            + self.arm_len * sample_ellipse(self.radii, self.x_rotation, self.angle0 + FRAC_PI_2);
141        let p3 = sample_ellipse(self.radii, self.x_rotation, angle1);
142        let p2 =
143            p3 - self.arm_len * sample_ellipse(self.radii, self.x_rotation, angle1 + FRAC_PI_2);
144
145        self.angle0 = angle1;
146        self.p0 = p3;
147        self.idx += 1;
148
149        Some(PathEl::CurveTo(
150            self.center + p1,
151            self.center + p2,
152            self.center + p3,
153        ))
154    }
155}
156
157/// Take the ellipse radii, how the radii are rotated, and the sweep angle, and return a point on
158/// the ellipse.
159fn sample_ellipse(radii: Vec2, x_rotation: f64, angle: f64) -> Vec2 {
160    let (angle_sin, angle_cos) = angle.sin_cos();
161    let u = radii.x * angle_cos;
162    let v = radii.y * angle_sin;
163    rotate_pt(Vec2::new(u, v), x_rotation)
164}
165
166/// Rotate `pt` about the origin by `angle` radians.
167fn rotate_pt(pt: Vec2, angle: f64) -> Vec2 {
168    let (angle_sin, angle_cos) = angle.sin_cos();
169    Vec2::new(
170        pt.x * angle_cos - pt.y * angle_sin,
171        pt.x * angle_sin + pt.y * angle_cos,
172    )
173}
174
175impl Shape for Arc {
176    type PathElementsIter<'iter> = iter::Chain<iter::Once<PathEl>, ArcAppendIter>;
177
178    fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter<'_> {
179        let p0 = sample_ellipse(self.radii, self.x_rotation, self.start_angle);
180        iter::once(PathEl::MoveTo(self.center + p0)).chain(self.append_iter(tolerance))
181    }
182
183    /// Note: shape isn't closed so area is not well defined.
184    #[inline]
185    fn area(&self) -> f64 {
186        let Vec2 { x, y } = self.radii;
187        PI * x * y
188    }
189
190    /// The perimeter of the arc.
191    ///
192    /// For now we just approximate by using the bezier curve representation.
193    #[inline]
194    fn perimeter(&self, accuracy: f64) -> f64 {
195        self.path_segments(0.1).perimeter(accuracy)
196    }
197
198    /// Note: shape isn't closed, so a point's winding number is not well defined.
199    #[inline]
200    fn winding(&self, pt: Point) -> i32 {
201        self.path_segments(0.1).winding(pt)
202    }
203
204    #[inline]
205    fn bounding_box(&self) -> Rect {
206        self.path_segments(0.1).bounding_box()
207    }
208}
209
210impl Mul<Arc> for Affine {
211    type Output = Arc;
212
213    fn mul(self, arc: Arc) -> Self::Output {
214        let ellipse = self * Ellipse::new(arc.center, arc.radii, arc.x_rotation);
215        let center = ellipse.center();
216        let (radii, rotation) = ellipse.radii_and_rotation();
217        Arc {
218            center,
219            radii,
220            x_rotation: rotation,
221            start_angle: arc.start_angle,
222            sweep_angle: arc.sweep_angle,
223        }
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    #[test]
231    fn reversed_arc() {
232        let a = Arc::new((0., 0.), (1., 0.), 0., PI, 0.);
233        let f = a.reversed();
234
235        // Most fields should be unchanged:
236        assert_eq!(a.center, f.center);
237        assert_eq!(a.radii, f.radii);
238        assert_eq!(a.x_rotation, f.x_rotation);
239
240        // Sweep angle should be in reverse
241        assert_eq!(a.sweep_angle, -f.sweep_angle);
242
243        // Reversing it again should result in the original arc
244        assert_eq!(a, f.reversed());
245    }
246}