1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use use_coordinate::GeometryError;
5use use_point::Point2;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Orientation2 {
10 Clockwise,
12 CounterClockwise,
14 Collinear,
16}
17
18#[must_use]
20pub fn signed_twice_area_2d(a: Point2, b: Point2, c: Point2) -> f64 {
21 let ab_x = b.x() - a.x();
22 let ab_y = b.y() - a.y();
23 let ac_x = c.x() - a.x();
24 let ac_y = c.y() - a.y();
25
26 ab_x.mul_add(ac_y, -(ab_y * ac_x))
27}
28
29#[must_use]
31pub fn orientation_2d(a: Point2, b: Point2, c: Point2) -> Orientation2 {
32 let signed_area = signed_twice_area_2d(a, b, c);
33
34 if signed_area > 0.0 {
35 Orientation2::CounterClockwise
36 } else if signed_area < 0.0 {
37 Orientation2::Clockwise
38 } else {
39 Orientation2::Collinear
40 }
41}
42
43pub fn orientation_2d_with_tolerance(
52 a: Point2,
53 b: Point2,
54 c: Point2,
55 tolerance: f64,
56) -> Result<Orientation2, GeometryError> {
57 let tolerance = GeometryError::validate_tolerance(tolerance)?;
58 let signed_area = signed_twice_area_2d(a, b, c);
59
60 Ok(if signed_area > tolerance {
61 Orientation2::CounterClockwise
62 } else if signed_area < -tolerance {
63 Orientation2::Clockwise
64 } else {
65 Orientation2::Collinear
66 })
67}
68
69pub fn try_orientation_2d(a: Point2, b: Point2, c: Point2) -> Result<Orientation2, GeometryError> {
76 let a = a.validate()?;
77 let b = b.validate()?;
78 let c = c.validate()?;
79
80 Ok(orientation_2d(a, b, c))
81}
82
83pub fn try_orientation_2d_with_tolerance(
95 a: Point2,
96 b: Point2,
97 c: Point2,
98 tolerance: f64,
99) -> Result<Orientation2, GeometryError> {
100 let a = a.validate()?;
101 let b = b.validate()?;
102 let c = c.validate()?;
103
104 orientation_2d_with_tolerance(a, b, c, tolerance)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::{
110 Orientation2, orientation_2d, orientation_2d_with_tolerance, signed_twice_area_2d,
111 try_orientation_2d, try_orientation_2d_with_tolerance,
112 };
113 use use_coordinate::GeometryError;
114 use use_point::Point2;
115
116 fn approx_eq(left: f64, right: f64) -> bool {
117 (left - right).abs() < 1.0e-10
118 }
119
120 #[test]
121 fn computes_counterclockwise_orientation() {
122 assert_eq!(
123 orientation_2d(
124 Point2::new(0.0, 0.0),
125 Point2::new(4.0, 0.0),
126 Point2::new(0.0, 3.0)
127 ),
128 Orientation2::CounterClockwise
129 );
130 }
131
132 #[test]
133 fn computes_clockwise_orientation() {
134 assert_eq!(
135 orientation_2d(
136 Point2::new(0.0, 0.0),
137 Point2::new(0.0, 3.0),
138 Point2::new(4.0, 0.0)
139 ),
140 Orientation2::Clockwise
141 );
142 }
143
144 #[test]
145 fn computes_collinear_orientation() {
146 assert_eq!(
147 orientation_2d(
148 Point2::new(0.0, 0.0),
149 Point2::new(1.0, 1.0),
150 Point2::new(2.0, 2.0)
151 ),
152 Orientation2::Collinear
153 );
154 }
155
156 #[test]
157 fn computes_signed_twice_area() {
158 assert!(approx_eq(
159 signed_twice_area_2d(
160 Point2::new(0.0, 0.0),
161 Point2::new(4.0, 0.0),
162 Point2::new(0.0, 3.0)
163 ),
164 12.0
165 ));
166 }
167
168 #[test]
169 fn computes_try_orientation_for_finite_points() {
170 assert_eq!(
171 try_orientation_2d(
172 Point2::new(0.0, 0.0),
173 Point2::new(4.0, 0.0),
174 Point2::new(0.0, 3.0)
175 ),
176 Ok(Orientation2::CounterClockwise)
177 );
178 }
179
180 #[test]
181 fn rejects_try_orientation_for_non_finite_points() {
182 assert_eq!(
183 try_orientation_2d(
184 Point2::new(0.0, 0.0),
185 Point2::new(4.0, 0.0),
186 Point2::new(0.0, f64::INFINITY)
187 ),
188 Err(GeometryError::NonFiniteComponent {
189 type_name: "Point2",
190 component: "y",
191 value: f64::INFINITY,
192 })
193 );
194 }
195
196 #[test]
197 fn computes_tolerance_based_orientation() {
198 assert_eq!(
199 orientation_2d_with_tolerance(
200 Point2::new(0.0, 0.0),
201 Point2::new(1.0, 1.0),
202 Point2::new(2.0, 2.0 + 1.0e-12),
203 1.0e-11
204 ),
205 Ok(Orientation2::Collinear)
206 );
207 assert_eq!(
208 try_orientation_2d_with_tolerance(
209 Point2::new(0.0, 0.0),
210 Point2::new(4.0, 0.0),
211 Point2::new(0.0, 3.0),
212 0.0
213 ),
214 Ok(Orientation2::CounterClockwise)
215 );
216 }
217
218 #[test]
219 fn rejects_invalid_orientation_tolerance() {
220 assert_eq!(
221 orientation_2d_with_tolerance(
222 Point2::new(0.0, 0.0),
223 Point2::new(1.0, 0.0),
224 Point2::new(0.0, 1.0),
225 -1.0
226 ),
227 Err(GeometryError::NegativeTolerance(-1.0))
228 );
229 }
230}