Skip to main content

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, ParamCurve, PathEl, Point, Rect, Shape, Vec2};
7use core::{
8    f64::consts::{FRAC_PI_2, PI},
9    iter,
10    ops::{Mul, Range},
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    #[inline]
70    fn angle_at(&self, t: f64) -> f64 {
71        self.start_angle + self.sweep_angle * t
72    }
73
74    /// Create an iterator generating Bezier path elements.
75    ///
76    /// The generated elements can be appended to an existing bezier path.
77    pub fn append_iter(&self, tolerance: f64) -> ArcAppendIter {
78        let sign = self.sweep_angle.signum();
79        let scaled_err = self.radii.x.max(self.radii.y) / tolerance;
80        // Number of subdivisions per ellipse based on error tolerance.
81        // Note: this may slightly underestimate the error for quadrants.
82        let n_err = (1.1163 * scaled_err).powf(1.0 / 6.0).max(3.999_999);
83        let n = (n_err * self.sweep_angle.abs() * (1.0 / (2.0 * PI))).ceil();
84        let angle_step = self.sweep_angle / n;
85        let n = n as usize;
86        let arm_len = (4.0 / 3.0) * (0.25 * angle_step).abs().tan() * sign;
87        let angle0 = self.start_angle;
88        let p0 = sample_ellipse(self.radii, self.x_rotation, angle0);
89
90        ArcAppendIter {
91            idx: 0,
92
93            center: self.center,
94            radii: self.radii,
95            x_rotation: self.x_rotation,
96            n,
97            arm_len,
98            angle_step,
99
100            p0,
101            angle0,
102        }
103    }
104
105    /// Converts an `Arc` into a series of cubic bezier segments.
106    ///
107    /// The closure `p` will be invoked with the control points for each segment.
108    pub fn to_cubic_beziers<P>(self, tolerance: f64, mut p: P)
109    where
110        P: FnMut(Point, Point, Point),
111    {
112        let mut path = self.append_iter(tolerance);
113        while let Some(PathEl::CurveTo(p1, p2, p3)) = path.next() {
114            p(p1, p2, p3);
115        }
116    }
117}
118
119#[doc(hidden)]
120pub struct ArcAppendIter {
121    idx: usize,
122
123    center: Point,
124    radii: Vec2,
125    x_rotation: f64,
126    n: usize,
127    arm_len: f64,
128    angle_step: f64,
129
130    p0: Vec2,
131    angle0: f64,
132}
133
134impl Iterator for ArcAppendIter {
135    type Item = PathEl;
136
137    fn next(&mut self) -> Option<Self::Item> {
138        if self.idx >= self.n {
139            return None;
140        }
141
142        let angle1 = self.angle0 + self.angle_step;
143        let p0 = self.p0;
144        let p1 = p0
145            + self.arm_len * sample_ellipse(self.radii, self.x_rotation, self.angle0 + FRAC_PI_2);
146        let p3 = sample_ellipse(self.radii, self.x_rotation, angle1);
147        let p2 =
148            p3 - self.arm_len * sample_ellipse(self.radii, self.x_rotation, angle1 + FRAC_PI_2);
149
150        self.angle0 = angle1;
151        self.p0 = p3;
152        self.idx += 1;
153
154        Some(PathEl::CurveTo(
155            self.center + p1,
156            self.center + p2,
157            self.center + p3,
158        ))
159    }
160}
161
162/// Take the ellipse radii, how the radii are rotated, and the sweep angle, and return a point on
163/// the ellipse.
164fn sample_ellipse(radii: Vec2, x_rotation: f64, angle: f64) -> Vec2 {
165    let (angle_sin, angle_cos) = angle.sin_cos();
166    let u = radii.x * angle_cos;
167    let v = radii.y * angle_sin;
168    rotate_pt(Vec2::new(u, v), x_rotation)
169}
170
171/// Rotate `pt` about the origin by `angle` radians.
172fn rotate_pt(pt: Vec2, angle: f64) -> Vec2 {
173    let (angle_sin, angle_cos) = angle.sin_cos();
174    Vec2::new(
175        pt.x * angle_cos - pt.y * angle_sin,
176        pt.x * angle_sin + pt.y * angle_cos,
177    )
178}
179
180impl ParamCurve for Arc {
181    fn eval(&self, t: f64) -> Point {
182        self.center + sample_ellipse(self.radii, self.x_rotation, self.angle_at(t))
183    }
184
185    fn subsegment(&self, range: Range<f64>) -> Self {
186        Self {
187            center: self.center,
188            radii: self.radii,
189            start_angle: self.angle_at(range.start),
190            sweep_angle: self.sweep_angle * (range.end - range.start),
191            x_rotation: self.x_rotation,
192        }
193    }
194}
195
196impl Shape for Arc {
197    type PathElementsIter<'iter> = iter::Chain<iter::Once<PathEl>, ArcAppendIter>;
198
199    fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter<'_> {
200        iter::once(PathEl::MoveTo(self.start())).chain(self.append_iter(tolerance))
201    }
202
203    /// Note: shape isn't closed so area is not well defined.
204    #[inline]
205    fn area(&self) -> f64 {
206        let Vec2 { x, y } = self.radii;
207        PI * x * y
208    }
209
210    /// The perimeter of the arc.
211    ///
212    /// For now we just approximate by using the bezier curve representation.
213    #[inline]
214    fn perimeter(&self, accuracy: f64) -> f64 {
215        self.path_segments(0.1).perimeter(accuracy)
216    }
217
218    /// Note: shape isn't closed, so a point's winding number is not well defined.
219    #[inline]
220    fn winding(&self, pt: Point) -> i32 {
221        self.path_segments(0.1).winding(pt)
222    }
223
224    #[inline]
225    fn bounding_box(&self) -> Rect {
226        self.path_segments(0.1).bounding_box()
227    }
228}
229
230impl Mul<Arc> for Affine {
231    type Output = Arc;
232
233    fn mul(self, arc: Arc) -> Self::Output {
234        let ellipse = self * Ellipse::new(arc.center, arc.radii, arc.x_rotation);
235        let center = ellipse.center();
236        let (radii, rotation) = ellipse.radii_and_rotation();
237        Arc {
238            center,
239            radii,
240            x_rotation: rotation,
241            start_angle: arc.start_angle,
242            sweep_angle: arc.sweep_angle,
243        }
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use core::f64::consts::{FRAC_PI_4, FRAC_PI_6};
250
251    use crate::ParamCurve;
252
253    use super::*;
254
255    fn assert_point_near(actual: Point, expected: Point) {
256        let epsilon = 1e-12;
257        assert!(
258            actual.distance(expected) <= epsilon,
259            "expected {expected:?}, got {actual:?}"
260        );
261    }
262
263    fn assert_subsegment_matches(arc: Arc, range: Range<f64>) {
264        let subsegment = arc.subsegment(range.clone());
265        for t in [0.0, 0.25, 0.5, 1.0] {
266            let expected_t = range.start + (range.end - range.start) * t;
267            assert_point_near(subsegment.eval(t), arc.eval(expected_t));
268        }
269        assert_point_near(subsegment.start(), arc.eval(range.start));
270        assert_point_near(subsegment.end(), arc.eval(range.end));
271    }
272
273    #[test]
274    fn reversed_arc() {
275        let a = Arc::new((0., 0.), (1., 0.), 0., PI, 0.);
276        let f = a.reversed();
277
278        // Most fields should be unchanged:
279        assert_eq!(a.center, f.center);
280        assert_eq!(a.radii, f.radii);
281        assert_eq!(a.x_rotation, f.x_rotation);
282
283        // Sweep angle should be in reverse
284        assert_eq!(a.sweep_angle, -f.sweep_angle);
285
286        // Reversing it again should result in the original arc
287        assert_eq!(a, f.reversed());
288    }
289
290    #[test]
291    fn eval_endpoints_and_midpoint() {
292        let arc = Arc::new((3.0, -2.0), (4.0, 1.5), FRAC_PI_6, PI, 0.0);
293        assert_point_near(arc.eval(0.0), arc.start());
294        assert_point_near(arc.eval(1.0), arc.end());
295        assert_point_near(
296            arc.eval(0.5),
297            arc.center + sample_ellipse(arc.radii, arc.x_rotation, arc.angle_at(0.5)),
298        );
299    }
300
301    #[test]
302    fn eval_rotated_ellipse() {
303        let arc = Arc::new((5.0, 7.0), (3.0, 2.0), FRAC_PI_6, PI / 3.0, FRAC_PI_4);
304        assert_point_near(
305            arc.start(),
306            arc.center + sample_ellipse(arc.radii, arc.x_rotation, arc.start_angle),
307        );
308        assert_point_near(
309            arc.end(),
310            arc.center
311                + sample_ellipse(arc.radii, arc.x_rotation, arc.start_angle + arc.sweep_angle),
312        );
313    }
314
315    #[test]
316    fn eval_reversed_arc() {
317        let arc = Arc::new((2.0, 3.0), (4.0, 1.0), FRAC_PI_6, PI / 2.0, FRAC_PI_4);
318        let reversed = arc.reversed();
319        assert_point_near(reversed.start(), arc.end());
320        assert_point_near(reversed.end(), arc.start());
321        assert_point_near(reversed.eval(0.5), arc.eval(0.5));
322    }
323
324    #[test]
325    fn subsegment_matches_original() {
326        let arc = Arc::new((1.0, -4.0), (3.0, 2.0), -FRAC_PI_4, PI, FRAC_PI_6);
327        assert_subsegment_matches(arc, 0.2..0.7);
328    }
329
330    #[test]
331    fn subsegment_negative_sweep_matches_original() {
332        let arc = Arc::new((1.0, 2.0), (5.0, 3.0), PI, -0.75 * PI, FRAC_PI_6);
333        assert_subsegment_matches(arc, 0.1..0.8);
334    }
335
336    #[test]
337    fn subsegment_tiny_sweep_matches_original() {
338        let arc = Arc::new((0.0, 0.0), (2.0, 1.0), 1.0, 1e-9, FRAC_PI_4);
339        assert_subsegment_matches(arc, 0.25..0.75);
340    }
341}