fj_math/
arc.rs

1use num_traits::Float;
2
3use crate::{Point, Scalar, Vector};
4
5/// Calculated geometry that is useful when dealing with an arc
6pub struct Arc {
7    /// Center of the circle the arc is constructed on
8    pub center: Point<2>,
9
10    /// Radius of the circle the arc is constructed on
11    pub radius: Scalar,
12
13    /// Angle of `start` relative to `center`, in radians
14    pub start_angle: Scalar,
15
16    /// Angle of `end` relative to `center`, in radians
17    pub end_angle: Scalar,
18}
19
20impl Arc {
21    /// Constructs an [`Arc`] from two endpoints and the associated angle.
22    pub fn from_endpoints_and_angle(
23        p0: impl Into<Point<2>>,
24        p1: impl Into<Point<2>>,
25        angle_rad: Scalar,
26    ) -> Self {
27        let p0 = p0.into();
28        let p1 = p1.into();
29
30        // This is an adaptation of this:
31        // https://math.stackexchange.com/a/87374
32
33        let distance_between_endpoints = (p1 - p0).magnitude();
34        let more_than_half_turn = angle_rad.abs() > Scalar::PI;
35
36        let radius = distance_between_endpoints
37            / (2. * (angle_rad.abs().into_f64() / 2.).sin());
38
39        let center = {
40            let midpoint = Point {
41                coords: (p0.coords + p1.coords) / 2.,
42            };
43            let unit_vector_midpoint_to_center = {
44                let clockwise_turn = angle_rad <= Scalar::ZERO;
45                let f = match (clockwise_turn, more_than_half_turn) {
46                    (false, false) | (true, true) => Scalar::ONE,
47                    (false, true) | (true, false) => -Scalar::ONE,
48                };
49
50                let unit_vector_p0_to_p1 =
51                    (p1 - p0) / distance_between_endpoints * f;
52
53                Vector::from([-unit_vector_p0_to_p1.v, unit_vector_p0_to_p1.u])
54            };
55            let distance_center_to_midpoint = (radius.powi(2)
56                - (distance_between_endpoints.powi(2) / 4.))
57                .sqrt();
58
59            midpoint
60                + unit_vector_midpoint_to_center * distance_center_to_midpoint
61        };
62
63        let start_angle = {
64            let from_center = p0 - center;
65            from_center.v.atan2(from_center.u)
66        };
67        let end_angle = {
68            let from_center = p1 - center;
69            let offset = if more_than_half_turn {
70                Scalar::TAU
71            } else {
72                Scalar::ZERO
73            };
74
75            from_center.v.atan2(from_center.u) + offset
76        };
77        Self {
78            center,
79            radius,
80            start_angle,
81            end_angle,
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use crate::{Point, Scalar, Vector};
89
90    use super::Arc;
91
92    use approx::{assert_abs_diff_eq, AbsDiffEq};
93
94    #[test]
95    fn arc_construction() {
96        check_arc_calculation(
97            [0., 0.],
98            1.,
99            0_f64.to_radians(),
100            90_f64.to_radians(),
101        );
102        check_arc_calculation(
103            [-4., 2.],
104            1.5,
105            5_f64.to_radians(),
106            -5_f64.to_radians(),
107        );
108        check_arc_calculation(
109            [3., 8.],
110            3.,
111            0_f64.to_radians(),
112            100_f64.to_radians(),
113        );
114        check_arc_calculation(
115            [1., -1.],
116            1.,
117            90_f64.to_radians(),
118            180_f64.to_radians(),
119        );
120        check_arc_calculation(
121            [0., 0.],
122            1.,
123            0_f64.to_radians(),
124            270_f64.to_radians(),
125        );
126    }
127
128    fn check_arc_calculation(
129        center: impl Into<Point<2>>,
130        radius: f64,
131        a0: f64,
132        a1: f64,
133    ) {
134        let center = center.into();
135        let angle = a1 - a0;
136
137        let p0 = center + Vector::from([a0.cos(), a0.sin()]) * radius;
138        let p1 = center + Vector::from([a1.cos(), a1.sin()]) * radius;
139
140        let arc = Arc::from_endpoints_and_angle(p0, p1, Scalar::from(angle));
141
142        let epsilon = Scalar::default_epsilon() * 10.;
143
144        dbg!(arc.start_angle);
145        dbg!(arc.end_angle);
146        assert_abs_diff_eq!(arc.center, center, epsilon = epsilon);
147        assert_abs_diff_eq!(
148            arc.radius,
149            Scalar::from(radius),
150            epsilon = epsilon
151        );
152
153        assert_abs_diff_eq!(
154            arc.start_angle,
155            Scalar::from(a0),
156            epsilon = epsilon
157        );
158        assert_abs_diff_eq!(arc.end_angle, Scalar::from(a1), epsilon = epsilon);
159    }
160}