1use crate::{error::GeometryError, point::Point2, vector::Vector2};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Orientation2 {
6 Clockwise,
8 CounterClockwise,
10 Collinear,
12}
13
14#[must_use]
16pub fn signed_twice_area_2d(a: Point2, b: Point2, c: Point2) -> f64 {
17 Vector2::from_points(a, b).cross(Vector2::from_points(a, c))
18}
19
20#[must_use]
22pub fn orientation_2d(a: Point2, b: Point2, c: Point2) -> Orientation2 {
23 let signed_area = signed_twice_area_2d(a, b, c);
24
25 if signed_area > 0.0 {
26 Orientation2::CounterClockwise
27 } else if signed_area < 0.0 {
28 Orientation2::Clockwise
29 } else {
30 Orientation2::Collinear
31 }
32}
33
34pub fn orientation_2d_with_tolerance(
43 a: Point2,
44 b: Point2,
45 c: Point2,
46 tolerance: f64,
47) -> Result<Orientation2, GeometryError> {
48 let tolerance = GeometryError::validate_tolerance(tolerance)?;
49 let signed_area = signed_twice_area_2d(a, b, c);
50
51 Ok(if signed_area > tolerance {
52 Orientation2::CounterClockwise
53 } else if signed_area < -tolerance {
54 Orientation2::Clockwise
55 } else {
56 Orientation2::Collinear
57 })
58}
59
60pub fn try_orientation_2d(a: Point2, b: Point2, c: Point2) -> Result<Orientation2, GeometryError> {
67 let a = a.validate()?;
68 let b = b.validate()?;
69 let c = c.validate()?;
70
71 Ok(orientation_2d(a, b, c))
72}
73
74pub fn try_orientation_2d_with_tolerance(
86 a: Point2,
87 b: Point2,
88 c: Point2,
89 tolerance: f64,
90) -> Result<Orientation2, GeometryError> {
91 let a = a.validate()?;
92 let b = b.validate()?;
93 let c = c.validate()?;
94
95 orientation_2d_with_tolerance(a, b, c, tolerance)
96}
97
98#[cfg(test)]
99mod tests {
100 use super::{
101 Orientation2, orientation_2d, orientation_2d_with_tolerance, signed_twice_area_2d,
102 try_orientation_2d, try_orientation_2d_with_tolerance,
103 };
104 use crate::{error::GeometryError, point::Point2};
105
106 fn approx_eq(left: f64, right: f64) -> bool {
107 (left - right).abs() < 1.0e-10
108 }
109
110 #[test]
111 fn computes_counterclockwise_orientation() {
112 assert_eq!(
113 orientation_2d(
114 Point2::new(0.0, 0.0),
115 Point2::new(4.0, 0.0),
116 Point2::new(0.0, 3.0)
117 ),
118 Orientation2::CounterClockwise
119 );
120 }
121
122 #[test]
123 fn computes_clockwise_orientation() {
124 assert_eq!(
125 orientation_2d(
126 Point2::new(0.0, 0.0),
127 Point2::new(0.0, 3.0),
128 Point2::new(4.0, 0.0)
129 ),
130 Orientation2::Clockwise
131 );
132 }
133
134 #[test]
135 fn computes_collinear_orientation() {
136 assert_eq!(
137 orientation_2d(
138 Point2::new(0.0, 0.0),
139 Point2::new(1.0, 1.0),
140 Point2::new(2.0, 2.0)
141 ),
142 Orientation2::Collinear
143 );
144 }
145
146 #[test]
147 fn computes_signed_twice_area() {
148 assert!(approx_eq(
149 signed_twice_area_2d(
150 Point2::new(0.0, 0.0),
151 Point2::new(4.0, 0.0),
152 Point2::new(0.0, 3.0)
153 ),
154 12.0
155 ));
156 }
157
158 #[test]
159 fn computes_try_orientation_for_finite_points() {
160 assert_eq!(
161 try_orientation_2d(
162 Point2::new(0.0, 0.0),
163 Point2::new(4.0, 0.0),
164 Point2::new(0.0, 3.0)
165 ),
166 Ok(Orientation2::CounterClockwise)
167 );
168 }
169
170 #[test]
171 fn rejects_try_orientation_for_non_finite_points() {
172 assert_eq!(
173 try_orientation_2d(
174 Point2::new(0.0, 0.0),
175 Point2::new(4.0, 0.0),
176 Point2::new(0.0, f64::INFINITY)
177 ),
178 Err(GeometryError::NonFiniteComponent {
179 type_name: "Point2",
180 component: "y",
181 value: f64::INFINITY,
182 })
183 );
184 }
185
186 #[test]
187 fn computes_tolerance_based_orientation() {
188 assert_eq!(
189 orientation_2d_with_tolerance(
190 Point2::new(0.0, 0.0),
191 Point2::new(1.0, 1.0),
192 Point2::new(2.0, 2.0 + 1.0e-12),
193 1.0e-11
194 ),
195 Ok(Orientation2::Collinear)
196 );
197 assert_eq!(
198 try_orientation_2d_with_tolerance(
199 Point2::new(0.0, 0.0),
200 Point2::new(4.0, 0.0),
201 Point2::new(0.0, 3.0),
202 0.0
203 ),
204 Ok(Orientation2::CounterClockwise)
205 );
206 }
207
208 #[test]
209 fn rejects_invalid_orientation_tolerance() {
210 assert_eq!(
211 orientation_2d_with_tolerance(
212 Point2::new(0.0, 0.0),
213 Point2::new(1.0, 0.0),
214 Point2::new(0.0, 1.0),
215 -1.0
216 ),
217 Err(GeometryError::NegativeTolerance(-1.0))
218 );
219 }
220}