rsvg/
angle.rs

1//! CSS angle values.
2
3use std::f64::consts::*;
4
5use cssparser::{Parser, Token};
6use float_cmp::approx_eq;
7
8use crate::error::*;
9use crate::parsers::{Parse, finite_f32};
10
11#[derive(Debug, Copy, Clone, PartialEq)]
12pub struct Angle(f64);
13
14impl Angle {
15    pub fn new(rad: f64) -> Angle {
16        Angle(Angle::normalize(rad))
17    }
18
19    pub fn from_degrees(deg: f64) -> Angle {
20        Angle(Angle::normalize(deg.to_radians()))
21    }
22
23    pub fn from_vector(vx: f64, vy: f64) -> Angle {
24        let rad = vy.atan2(vx);
25
26        if rad.is_nan() {
27            Angle(0.0)
28        } else {
29            Angle(Angle::normalize(rad))
30        }
31    }
32
33    pub fn radians(self) -> f64 {
34        self.0
35    }
36
37    pub fn bisect(self, other: Angle) -> Angle {
38        let half_delta = (other.0 - self.0) * 0.5;
39
40        if FRAC_PI_2 < half_delta.abs() {
41            Angle(Angle::normalize(self.0 + half_delta - PI))
42        } else {
43            Angle(Angle::normalize(self.0 + half_delta))
44        }
45    }
46
47    //Flips an angle to be 180deg or PI radians rotated
48    pub fn flip(self) -> Angle {
49        Angle::new(self.radians() + PI)
50    }
51
52    // Normalizes an angle to [0.0, 2*PI)
53    fn normalize(rad: f64) -> f64 {
54        let res = rad % (PI * 2.0);
55        if approx_eq!(f64, res, 0.0) {
56            0.0
57        } else if res < 0.0 {
58            res + PI * 2.0
59        } else {
60            res
61        }
62    }
63}
64
65// angle:
66// https://www.w3.org/TR/SVG/types.html#DataTypeAngle
67//
68// angle ::= number ("deg" | "grad" | "rad")?
69//
70impl Parse for Angle {
71    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Angle, ParseError<'i>> {
72        let angle = {
73            let loc = parser.current_source_location();
74
75            let token = parser.next()?;
76
77            match *token {
78                Token::Number { value, .. } => {
79                    let degrees = finite_f32(value).map_err(|e| loc.new_custom_error(e))?;
80                    Angle::from_degrees(f64::from(degrees))
81                }
82
83                Token::Dimension {
84                    value, ref unit, ..
85                } => {
86                    let value = f64::from(finite_f32(value).map_err(|e| loc.new_custom_error(e))?);
87
88                    match unit.as_ref() {
89                        "deg" => Angle::from_degrees(value),
90                        "grad" => Angle::from_degrees(value * 360.0 / 400.0),
91                        "rad" => Angle::new(value),
92                        "turn" => Angle::from_degrees(value * 360.0),
93                        _ => {
94                            return Err(loc.new_unexpected_token_error(token.clone()));
95                        }
96                    }
97                }
98
99                _ => return Err(loc.new_unexpected_token_error(token.clone())),
100            }
101        };
102
103        Ok(angle)
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn parses_angle() {
113        assert_eq!(Angle::parse_str("0").unwrap(), Angle::new(0.0));
114        assert_eq!(Angle::parse_str("15").unwrap(), Angle::from_degrees(15.0));
115        assert_eq!(
116            Angle::parse_str("180.5deg").unwrap(),
117            Angle::from_degrees(180.5)
118        );
119        assert_eq!(Angle::parse_str("1rad").unwrap(), Angle::new(1.0));
120        assert_eq!(
121            Angle::parse_str("-400grad").unwrap(),
122            Angle::from_degrees(-360.0)
123        );
124        assert_eq!(
125            Angle::parse_str("0.25turn").unwrap(),
126            Angle::from_degrees(90.0)
127        );
128
129        assert!(Angle::parse_str("").is_err());
130        assert!(Angle::parse_str("foo").is_err());
131        assert!(Angle::parse_str("300foo").is_err());
132    }
133
134    fn test_bisection_angle(
135        expected: f64,
136        incoming_vx: f64,
137        incoming_vy: f64,
138        outgoing_vx: f64,
139        outgoing_vy: f64,
140    ) {
141        let i = Angle::from_vector(incoming_vx, incoming_vy);
142        let o = Angle::from_vector(outgoing_vx, outgoing_vy);
143        let bisected = i.bisect(o);
144        assert!(approx_eq!(f64, expected, bisected.radians()));
145    }
146
147    #[test]
148    fn bisection_angle_is_correct_from_incoming_counterclockwise_to_outgoing() {
149        // 1st quadrant
150        test_bisection_angle(FRAC_PI_4, 1.0, 0.0, 0.0, 1.0);
151
152        // 2nd quadrant
153        test_bisection_angle(FRAC_PI_2 + FRAC_PI_4, 0.0, 1.0, -1.0, 0.0);
154
155        // 3rd quadrant
156        test_bisection_angle(PI + FRAC_PI_4, -1.0, 0.0, 0.0, -1.0);
157
158        // 4th quadrant
159        test_bisection_angle(PI + FRAC_PI_2 + FRAC_PI_4, 0.0, -1.0, 1.0, 0.0);
160    }
161
162    #[test]
163    fn bisection_angle_is_correct_from_incoming_clockwise_to_outgoing() {
164        // 1st quadrant
165        test_bisection_angle(FRAC_PI_4, 0.0, 1.0, 1.0, 0.0);
166
167        // 2nd quadrant
168        test_bisection_angle(FRAC_PI_2 + FRAC_PI_4, -1.0, 0.0, 0.0, 1.0);
169
170        // 3rd quadrant
171        test_bisection_angle(PI + FRAC_PI_4, 0.0, -1.0, -1.0, 0.0);
172
173        // 4th quadrant
174        test_bisection_angle(PI + FRAC_PI_2 + FRAC_PI_4, 1.0, 0.0, 0.0, -1.0);
175    }
176
177    #[test]
178    fn bisection_angle_is_correct_for_more_than_quarter_turn_angle() {
179        test_bisection_angle(0.0, 0.1, -1.0, 0.1, 1.0);
180
181        test_bisection_angle(FRAC_PI_2, 1.0, 0.1, -1.0, 0.1);
182
183        test_bisection_angle(PI, -0.1, 1.0, -0.1, -1.0);
184
185        test_bisection_angle(PI + FRAC_PI_2, -1.0, -0.1, 1.0, -0.1);
186    }
187}