1use crate::robust::orient2d;
13
14pub fn signed_area(polygon: &[(f64, f64)]) -> f64 {
25 let n = polygon.len();
26 if n < 3 {
27 return 0.0;
28 }
29
30 let mut sum = 0.0;
32 let mut c = 0.0;
33
34 for i in 0..n {
35 let j = (i + 1) % n;
36 let term = polygon[i].0 * polygon[j].1 - polygon[j].0 * polygon[i].1;
37
38 let y = term - c;
39 let t = sum + y;
40 c = (t - sum) - y;
41 sum = t;
42 }
43
44 sum * 0.5
45}
46
47pub fn area(polygon: &[(f64, f64)]) -> f64 {
52 signed_area(polygon).abs()
53}
54
55pub fn centroid(polygon: &[(f64, f64)]) -> Option<(f64, f64)> {
66 let n = polygon.len();
67 if n < 3 {
68 return None;
69 }
70
71 let a = signed_area(polygon);
72 if a.abs() < 1e-15 {
73 return None;
74 }
75
76 let mut cx = 0.0;
77 let mut cy = 0.0;
78
79 for i in 0..n {
80 let j = (i + 1) % n;
81 let cross = polygon[i].0 * polygon[j].1 - polygon[j].0 * polygon[i].1;
82 cx += (polygon[i].0 + polygon[j].0) * cross;
83 cy += (polygon[i].1 + polygon[j].1) * cross;
84 }
85
86 let inv = 1.0 / (6.0 * a);
87 Some((cx * inv, cy * inv))
88}
89
90pub fn area_with_holes(exterior: &[(f64, f64)], holes: &[&[(f64, f64)]]) -> f64 {
97 let mut total = area(exterior);
98 for hole in holes {
99 total -= area(hole);
100 }
101 total.max(0.0)
102}
103
104pub fn centroid_with_holes(exterior: &[(f64, f64)], holes: &[&[(f64, f64)]]) -> Option<(f64, f64)> {
115 let ext_area = signed_area(exterior);
116 let ext_centroid = centroid(exterior)?;
117
118 if holes.is_empty() {
119 return Some(ext_centroid);
120 }
121
122 let mut weighted_cx = ext_area * ext_centroid.0;
123 let mut weighted_cy = ext_area * ext_centroid.1;
124 let mut total_area = ext_area;
125
126 for hole in holes {
127 let h_area = signed_area(hole);
128 if let Some((hcx, hcy)) = centroid(hole) {
129 weighted_cx -= h_area.abs() * hcx;
130 weighted_cy -= h_area.abs() * hcy;
131 total_area -= h_area.abs();
132 }
133 }
134
135 if total_area.abs() < 1e-15 {
136 return None;
137 }
138
139 Some((weighted_cx / total_area, weighted_cy / total_area))
140}
141
142pub fn perimeter(polygon: &[(f64, f64)]) -> f64 {
147 let n = polygon.len();
148 if n < 2 {
149 return 0.0;
150 }
151
152 let mut p = 0.0;
153 for i in 0..n {
154 let j = (i + 1) % n;
155 let dx = polygon[j].0 - polygon[i].0;
156 let dy = polygon[j].1 - polygon[i].1;
157 p += (dx * dx + dy * dy).sqrt();
158 }
159 p
160}
161
162pub fn convex_hull(points: &[(f64, f64)]) -> Vec<(f64, f64)> {
174 let n = points.len();
175 if n < 3 {
176 return points.to_vec();
177 }
178
179 let mut pts: Vec<(f64, f64)> = points.to_vec();
181 let mut pivot_idx = 0;
182 for (i, &(x, y)) in pts.iter().enumerate() {
183 let (px, py) = pts[pivot_idx];
184 if y < py || (y == py && x < px) {
185 pivot_idx = i;
186 }
187 }
188 pts.swap(0, pivot_idx);
189 let pivot = pts[0];
190
191 pts[1..].sort_by(|a, b| {
193 let o = orient2d(pivot, *a, *b);
194 match o {
195 crate::robust::Orientation::CounterClockwise => std::cmp::Ordering::Less,
196 crate::robust::Orientation::Clockwise => std::cmp::Ordering::Greater,
197 crate::robust::Orientation::Collinear => {
198 let da = (a.0 - pivot.0).powi(2) + (a.1 - pivot.1).powi(2);
200 let db = (b.0 - pivot.0).powi(2) + (b.1 - pivot.1).powi(2);
201 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
202 }
203 }
204 });
205
206 let mut hull: Vec<(f64, f64)> = Vec::with_capacity(n);
208 for &p in &pts {
209 while hull.len() >= 2 {
210 let top = hull[hull.len() - 1];
211 let second = hull[hull.len() - 2];
212 if !orient2d(second, top, p).is_ccw() {
213 hull.pop();
214 } else {
215 break;
216 }
217 }
218 hull.push(p);
219 }
220
221 hull
222}
223
224pub fn is_convex(polygon: &[(f64, f64)]) -> bool {
234 let n = polygon.len();
235 if n < 3 {
236 return false;
237 }
238
239 let mut expected: Option<crate::robust::Orientation> = None;
240
241 for i in 0..n {
242 let p0 = polygon[i];
243 let p1 = polygon[(i + 1) % n];
244 let p2 = polygon[(i + 2) % n];
245
246 let o = orient2d(p0, p1, p2);
247
248 if o.is_collinear() {
249 continue;
250 }
251
252 match expected {
253 None => expected = Some(o),
254 Some(ref exp) if *exp != o => return false,
255 _ => {}
256 }
257 }
258
259 true
260}
261
262pub fn ensure_ccw(polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
269 if polygon.len() < 3 {
270 return polygon.to_vec();
271 }
272 if crate::robust::is_ccw(polygon) {
273 polygon.to_vec()
274 } else {
275 let mut reversed = polygon.to_vec();
276 reversed.reverse();
277 reversed
278 }
279}
280
281pub fn contains_point(polygon: &[(f64, f64)], point: (f64, f64)) -> bool {
291 let n = polygon.len();
292 if n < 3 {
293 return false;
294 }
295
296 let mut winding = 0i32;
297
298 for i in 0..n {
299 let j = (i + 1) % n;
300 let (ax, ay) = polygon[i];
301 let (bx, by) = polygon[j];
302
303 if ay <= point.1 {
304 if by > point.1 {
305 if orient2d((ax, ay), (bx, by), point).is_ccw() {
307 winding += 1;
308 }
309 }
310 } else if by <= point.1 {
311 if orient2d((ax, ay), (bx, by), point).is_cw() {
313 winding -= 1;
314 }
315 }
316 }
317
318 winding != 0
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
335 fn test_shoelace_unit_square_exact() {
336 let unit_square = [(0.0_f64, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
338 let a = area(&unit_square);
339 assert!(
340 (a - 1.0).abs() < 1e-15,
341 "unit square area must be exactly 1.0, got {a}"
342 );
343 }
344
345 #[test]
346 fn test_shoelace_unit_triangle_exact() {
347 let tri = [(0.0_f64, 0.0), (1.0, 0.0), (0.0, 1.0)];
349 let a = area(&tri);
350 assert!(
351 (a - 0.5).abs() < 1e-15,
352 "unit triangle area must be exactly 0.5, got {a}"
353 );
354 }
355
356 #[test]
357 fn test_shoelace_rectangle_exact() {
358 let rect = [(0.0_f64, 0.0), (3.0, 0.0), (3.0, 2.0), (0.0, 2.0)];
360 let a = area(&rect);
361 assert!(
362 (a - 6.0).abs() < 1e-15,
363 "3×2 rectangle area must be exactly 6.0, got {a}"
364 );
365 }
366
367 #[test]
368 fn test_signed_area_ccw_square() {
369 let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
370 let a = signed_area(&square);
371 assert!((a - 100.0).abs() < 1e-10);
372 }
373
374 #[test]
375 fn test_signed_area_cw_square() {
376 let square = [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
377 let a = signed_area(&square);
378 assert!((a - (-100.0)).abs() < 1e-10);
379 }
380
381 #[test]
382 fn test_area_triangle() {
383 let tri = [(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)];
384 assert!((area(&tri) - 50.0).abs() < 1e-10);
385 }
386
387 #[test]
388 fn test_area_degenerate() {
389 assert!((area(&[(0.0, 0.0), (1.0, 0.0)]) - 0.0).abs() < 1e-15);
390 assert!((area(&[]) - 0.0).abs() < 1e-15);
391 }
392
393 #[test]
396 fn test_centroid_square() {
397 let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
398 let (cx, cy) = centroid(&square).unwrap();
399 assert!((cx - 5.0).abs() < 1e-10);
400 assert!((cy - 5.0).abs() < 1e-10);
401 }
402
403 #[test]
404 fn test_centroid_triangle() {
405 let tri = [(0.0, 0.0), (6.0, 0.0), (3.0, 6.0)];
406 let (cx, cy) = centroid(&tri).unwrap();
407 assert!((cx - 3.0).abs() < 1e-10);
408 assert!((cy - 2.0).abs() < 1e-10);
409 }
410
411 #[test]
412 fn test_centroid_degenerate() {
413 assert!(centroid(&[(0.0, 0.0), (1.0, 0.0)]).is_none());
414 assert!(centroid(&[(0.0, 0.0), (1.0, 0.0), (2.0, 0.0)]).is_none());
416 }
417
418 #[test]
421 fn test_area_with_holes_no_holes() {
422 let ext = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)];
423 assert!((area_with_holes(&ext, &[]) - 10000.0).abs() < 1e-10);
424 }
425
426 #[test]
427 fn test_area_with_holes_one_hole() {
428 let ext = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)];
429 let hole: &[(f64, f64)] = &[(25.0, 25.0), (75.0, 25.0), (75.0, 75.0), (25.0, 75.0)];
430 assert!((area_with_holes(&ext, &[hole]) - 7500.0).abs() < 1e-10);
432 }
433
434 #[test]
435 fn test_area_with_holes_two_holes() {
436 let ext = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)];
437 let h1: &[(f64, f64)] = &[(10.0, 10.0), (30.0, 10.0), (30.0, 30.0), (10.0, 30.0)];
438 let h2: &[(f64, f64)] = &[(50.0, 50.0), (70.0, 50.0), (70.0, 70.0), (50.0, 70.0)];
439 assert!((area_with_holes(&ext, &[h1, h2]) - 9200.0).abs() < 1e-10);
441 }
442
443 #[test]
446 fn test_centroid_with_holes_no_holes() {
447 let ext = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
448 let (cx, cy) = centroid_with_holes(&ext, &[]).unwrap();
449 assert!((cx - 5.0).abs() < 1e-10);
450 assert!((cy - 5.0).abs() < 1e-10);
451 }
452
453 #[test]
454 fn test_centroid_with_holes_symmetric_hole() {
455 let ext = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)];
457 let hole: &[(f64, f64)] = &[(25.0, 25.0), (75.0, 25.0), (75.0, 75.0), (25.0, 75.0)];
458 let (cx, cy) = centroid_with_holes(&ext, &[hole]).unwrap();
459 assert!((cx - 50.0).abs() < 1e-6);
460 assert!((cy - 50.0).abs() < 1e-6);
461 }
462
463 #[test]
464 fn test_centroid_with_holes_asymmetric_hole() {
465 let ext = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)];
467 let hole: &[(f64, f64)] = &[(50.0, 50.0), (100.0, 50.0), (100.0, 100.0), (50.0, 100.0)];
468 let (cx, cy) = centroid_with_holes(&ext, &[hole]).unwrap();
469 assert!(cx < 50.0);
471 assert!(cy < 50.0);
472 }
473
474 #[test]
477 fn test_perimeter_square() {
478 let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
479 assert!((perimeter(&square) - 40.0).abs() < 1e-10);
480 }
481
482 #[test]
483 fn test_perimeter_empty() {
484 assert!((perimeter(&[]) - 0.0).abs() < 1e-15);
485 }
486
487 #[test]
490 fn test_convex_hull_square() {
491 let points = [
492 (0.0, 0.0),
493 (10.0, 0.0),
494 (10.0, 10.0),
495 (0.0, 10.0),
496 (5.0, 5.0), ];
498 let hull = convex_hull(&points);
499 assert_eq!(hull.len(), 4);
500 assert!((area(&hull) - 100.0).abs() < 1e-10);
502 }
503
504 #[test]
505 fn test_convex_hull_triangle() {
506 let points = [(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)];
507 let hull = convex_hull(&points);
508 assert_eq!(hull.len(), 3);
509 }
510
511 #[test]
512 fn test_convex_hull_collinear() {
513 let points = [(0.0, 0.0), (1.0, 0.0), (2.0, 0.0)];
514 let hull = convex_hull(&points);
515 assert!(hull.len() <= 3);
517 }
518
519 #[test]
520 fn test_convex_hull_l_shape() {
521 let l_shape = [
522 (0.0, 0.0),
523 (10.0, 0.0),
524 (10.0, 5.0),
525 (5.0, 5.0),
526 (5.0, 10.0),
527 (0.0, 10.0),
528 ];
529 let hull = convex_hull(&l_shape);
530 assert_eq!(hull.len(), 5);
532 }
533
534 #[test]
537 fn test_is_convex_square() {
538 let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
539 assert!(is_convex(&square));
540 }
541
542 #[test]
543 fn test_is_convex_triangle() {
544 let tri = [(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)];
545 assert!(is_convex(&tri));
546 }
547
548 #[test]
549 fn test_is_convex_l_shape() {
550 let l = [
551 (0.0, 0.0),
552 (20.0, 0.0),
553 (20.0, 10.0),
554 (10.0, 10.0),
555 (10.0, 20.0),
556 (0.0, 20.0),
557 ];
558 assert!(!is_convex(&l));
559 }
560
561 #[test]
562 fn test_is_convex_degenerate() {
563 assert!(!is_convex(&[(0.0, 0.0), (1.0, 0.0)]));
564 assert!(!is_convex(&[]));
565 }
566
567 #[test]
570 fn test_ensure_ccw_already_ccw() {
571 let ccw = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
572 let result = ensure_ccw(&ccw);
573 assert_eq!(result, ccw.to_vec());
574 }
575
576 #[test]
577 fn test_ensure_ccw_from_cw() {
578 let cw = [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
579 let result = ensure_ccw(&cw);
580 assert!(crate::robust::is_ccw(&result));
581 }
582
583 #[test]
586 fn test_contains_point_inside_square() {
587 let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
588 assert!(contains_point(&square, (5.0, 5.0)));
589 }
590
591 #[test]
592 fn test_contains_point_outside_square() {
593 let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
594 assert!(!contains_point(&square, (15.0, 5.0)));
595 }
596
597 #[test]
598 fn test_contains_point_concave_polygon() {
599 let l_shape = [
601 (0.0, 0.0),
602 (10.0, 0.0),
603 (10.0, 5.0),
604 (5.0, 5.0),
605 (5.0, 10.0),
606 (0.0, 10.0),
607 ];
608 assert!(contains_point(&l_shape, (2.0, 2.0)));
610 assert!(contains_point(&l_shape, (2.0, 8.0)));
611 assert!(!contains_point(&l_shape, (8.0, 8.0)));
613 }
614
615 #[test]
616 fn test_contains_point_triangle() {
617 let tri = [(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)];
618 assert!(contains_point(&tri, (5.0, 3.0)));
619 assert!(!contains_point(&tri, (20.0, 5.0)));
620 }
621}