1use 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 pub fn flip(self) -> Angle {
49 Angle::new(self.radians() + PI)
50 }
51
52 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
65impl 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 test_bisection_angle(FRAC_PI_4, 1.0, 0.0, 0.0, 1.0);
151
152 test_bisection_angle(FRAC_PI_2 + FRAC_PI_4, 0.0, 1.0, -1.0, 0.0);
154
155 test_bisection_angle(PI + FRAC_PI_4, -1.0, 0.0, 0.0, -1.0);
157
158 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 test_bisection_angle(FRAC_PI_4, 0.0, 1.0, 1.0, 0.0);
166
167 test_bisection_angle(FRAC_PI_2 + FRAC_PI_4, -1.0, 0.0, 0.0, 1.0);
169
170 test_bisection_angle(PI + FRAC_PI_4, 0.0, -1.0, -1.0, 0.0);
172
173 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}