1use robust::{orient2d as shewchuk_orient2d, Coord};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub enum Orientation {
25 CounterClockwise,
27 Clockwise,
29 Collinear,
31}
32
33impl Orientation {
34 #[inline]
36 pub fn is_ccw(self) -> bool {
37 matches!(self, Self::CounterClockwise)
38 }
39
40 #[inline]
42 pub fn is_cw(self) -> bool {
43 matches!(self, Self::Clockwise)
44 }
45
46 #[inline]
48 pub fn is_collinear(self) -> bool {
49 matches!(self, Self::Collinear)
50 }
51}
52
53#[inline]
78pub fn orient2d(pa: (f64, f64), pb: (f64, f64), pc: (f64, f64)) -> Orientation {
79 let result = shewchuk_orient2d(
80 Coord { x: pa.0, y: pa.1 },
81 Coord { x: pb.0, y: pb.1 },
82 Coord { x: pc.0, y: pc.1 },
83 );
84
85 if result > 0.0 {
86 Orientation::CounterClockwise
87 } else if result < 0.0 {
88 Orientation::Clockwise
89 } else {
90 Orientation::Collinear
91 }
92}
93
94#[inline]
99pub fn orient2d_raw(pa: (f64, f64), pb: (f64, f64), pc: (f64, f64)) -> f64 {
100 shewchuk_orient2d(
101 Coord { x: pa.0, y: pa.1 },
102 Coord { x: pb.0, y: pb.1 },
103 Coord { x: pc.0, y: pc.1 },
104 )
105}
106
107const FILTER_EPSILON: f64 = 1e-12;
109
110#[inline]
116pub fn orient2d_filtered(pa: (f64, f64), pb: (f64, f64), pc: (f64, f64)) -> Orientation {
117 let acx = pa.0 - pc.0;
118 let bcx = pb.0 - pc.0;
119 let acy = pa.1 - pc.1;
120 let bcy = pb.1 - pc.1;
121
122 let det = acx * bcy - acy * bcx;
123 let det_sum = (acx * bcy).abs() + (acy * bcx).abs();
124
125 if det.abs() > FILTER_EPSILON * det_sum {
126 return if det > 0.0 {
127 Orientation::CounterClockwise
128 } else {
129 Orientation::Clockwise
130 };
131 }
132
133 orient2d(pa, pb, pc)
134}
135
136pub fn point_in_triangle(p: (f64, f64), a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> bool {
144 let o1 = orient2d(a, b, p);
145 let o2 = orient2d(b, c, p);
146 let o3 = orient2d(c, a, p);
147
148 (o1 == Orientation::CounterClockwise
149 && o2 == Orientation::CounterClockwise
150 && o3 == Orientation::CounterClockwise)
151 || (o1 == Orientation::Clockwise
152 && o2 == Orientation::Clockwise
153 && o3 == Orientation::Clockwise)
154}
155
156pub fn point_in_triangle_inclusive(
161 p: (f64, f64),
162 a: (f64, f64),
163 b: (f64, f64),
164 c: (f64, f64),
165) -> bool {
166 let o1 = orient2d(a, b, p);
167 let o2 = orient2d(b, c, p);
168 let o3 = orient2d(c, a, p);
169
170 let has_ccw = o1.is_ccw() || o2.is_ccw() || o3.is_ccw();
171 let has_cw = o1.is_cw() || o2.is_cw() || o3.is_cw();
172
173 !(has_ccw && has_cw)
174}
175
176pub fn is_convex(polygon: &[(f64, f64)]) -> bool {
181 let n = polygon.len();
182 if n < 3 {
183 return false;
184 }
185
186 let mut expected: Option<Orientation> = None;
187
188 for i in 0..n {
189 let p0 = polygon[i];
190 let p1 = polygon[(i + 1) % n];
191 let p2 = polygon[(i + 2) % n];
192
193 let o = orient2d(p0, p1, p2);
194 if o.is_collinear() {
195 continue;
196 }
197
198 match expected {
199 None => expected = Some(o),
200 Some(e) if e != o => return false,
201 _ => {}
202 }
203 }
204
205 true
206}
207
208pub fn is_ccw(polygon: &[(f64, f64)]) -> bool {
216 if polygon.len() < 3 {
217 return false;
218 }
219
220 let mut min_idx = 0;
221 for (i, &(x, y)) in polygon.iter().enumerate() {
222 let (mx, my) = polygon[min_idx];
223 if y < my || (y == my && x < mx) {
224 min_idx = i;
225 }
226 }
227
228 let n = polygon.len();
229 let prev = polygon[(min_idx + n - 1) % n];
230 let curr = polygon[min_idx];
231 let next = polygon[(min_idx + 1) % n];
232
233 orient2d(prev, curr, next).is_ccw()
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_orient2d_ccw() {
242 assert_eq!(
243 orient2d((0.0, 0.0), (1.0, 0.0), (0.5, 1.0)),
244 Orientation::CounterClockwise
245 );
246 }
247
248 #[test]
249 fn test_orient2d_cw() {
250 assert_eq!(
251 orient2d((0.0, 0.0), (0.5, 1.0), (1.0, 0.0)),
252 Orientation::Clockwise
253 );
254 }
255
256 #[test]
257 fn test_orient2d_collinear() {
258 assert_eq!(
259 orient2d((0.0, 0.0), (1.0, 1.0), (2.0, 2.0)),
260 Orientation::Collinear
261 );
262 }
263
264 #[test]
265 fn test_orient2d_filtered_matches_exact() {
266 let cases = [
267 ((0.0, 0.0), (10.0, 0.0), (5.0, 10.0)),
268 ((0.0, 0.0), (1.0, 1.0), (2.0, 2.0)),
269 ((0.0, 0.0), (1.0, 0.0), (0.5, -1.0)),
270 ];
271 for (a, b, c) in &cases {
272 assert_eq!(orient2d(*a, *b, *c), orient2d_filtered(*a, *b, *c));
273 }
274 }
275
276 #[test]
277 fn test_point_in_triangle_inside() {
278 let a = (0.0, 0.0);
279 let b = (10.0, 0.0);
280 let c = (5.0, 10.0);
281 assert!(point_in_triangle((5.0, 3.0), a, b, c));
282 }
283
284 #[test]
285 fn test_point_in_triangle_outside() {
286 let a = (0.0, 0.0);
287 let b = (10.0, 0.0);
288 let c = (5.0, 10.0);
289 assert!(!point_in_triangle((20.0, 5.0), a, b, c));
290 }
291
292 #[test]
293 fn test_point_in_triangle_on_edge() {
294 let a = (0.0, 0.0);
295 let b = (10.0, 0.0);
296 let c = (5.0, 10.0);
297 assert!(!point_in_triangle((5.0, 0.0), a, b, c));
299 }
300
301 #[test]
302 fn test_point_in_triangle_inclusive() {
303 let a = (0.0, 0.0);
304 let b = (10.0, 0.0);
305 let c = (5.0, 10.0);
306 assert!(point_in_triangle_inclusive((5.0, 3.0), a, b, c));
307 assert!(point_in_triangle_inclusive((5.0, 0.0), a, b, c)); assert!(point_in_triangle_inclusive((0.0, 0.0), a, b, c)); assert!(!point_in_triangle_inclusive((20.0, 5.0), a, b, c));
310 }
311
312 #[test]
313 fn test_is_convex_square() {
314 let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
315 assert!(is_convex(&square));
316 }
317
318 #[test]
319 fn test_is_convex_l_shape() {
320 let l_shape = [
321 (0.0, 0.0),
322 (10.0, 0.0),
323 (10.0, 5.0),
324 (5.0, 5.0),
325 (5.0, 10.0),
326 (0.0, 10.0),
327 ];
328 assert!(!is_convex(&l_shape));
329 }
330
331 #[test]
332 fn test_is_convex_triangle() {
333 assert!(is_convex(&[(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)]));
334 }
335
336 #[test]
337 fn test_is_convex_degenerate() {
338 assert!(!is_convex(&[(0.0, 0.0), (1.0, 0.0)])); }
340
341 #[test]
342 fn test_is_ccw_square() {
343 assert!(is_ccw(&[
344 (0.0, 0.0),
345 (10.0, 0.0),
346 (10.0, 10.0),
347 (0.0, 10.0)
348 ]));
349 assert!(!is_ccw(&[
350 (0.0, 0.0),
351 (0.0, 10.0),
352 (10.0, 10.0),
353 (10.0, 0.0)
354 ]));
355 }
356
357 #[test]
358 fn test_orientation_methods() {
359 assert!(Orientation::CounterClockwise.is_ccw());
360 assert!(!Orientation::CounterClockwise.is_cw());
361 assert!(Orientation::Clockwise.is_cw());
362 assert!(Orientation::Collinear.is_collinear());
363 }
364
365 #[test]
366 fn test_orient2d_extreme_coords() {
367 assert_eq!(
369 orient2d((1e10, 1e10), (1e10 + 1.0, 1e10), (1e10 + 0.5, 1e10 + 1.0)),
370 Orientation::CounterClockwise
371 );
372 }
373
374 #[test]
375 fn test_degenerate_triangle() {
376 assert!(!point_in_triangle(
378 (5.0, 0.0),
379 (0.0, 0.0),
380 (5.0, 0.0),
381 (10.0, 0.0)
382 ));
383 }
384}