1#![allow(clippy::if_same_then_else, clippy::ptr_arg)]
2#![allow(dead_code)]
19
20use std::f64::consts::PI;
21
22#[derive(Clone, Copy, Debug, PartialEq)]
28pub struct Point2 {
29 pub x: f64,
31 pub y: f64,
33}
34
35impl Point2 {
36 pub fn new(x: f64, y: f64) -> Self {
38 Self { x, y }
39 }
40
41 pub fn zero() -> Self {
43 Self { x: 0.0, y: 0.0 }
44 }
45
46 pub fn distance(&self, other: &Self) -> f64 {
48 let dx = self.x - other.x;
49 let dy = self.y - other.y;
50 (dx * dx + dy * dy).sqrt()
51 }
52
53 pub fn length(&self) -> f64 {
55 (self.x * self.x + self.y * self.y).sqrt()
56 }
57
58 pub fn normalized(&self) -> Self {
60 let l = self.length();
61 if l < 1.0e-14 {
62 return Self::zero();
63 }
64 Self {
65 x: self.x / l,
66 y: self.y / l,
67 }
68 }
69
70 pub fn dot(&self, other: &Self) -> f64 {
72 self.x * other.x + self.y * other.y
73 }
74
75 pub fn cross(&self, other: &Self) -> f64 {
77 self.x * other.y - self.y * other.x
78 }
79
80 pub fn perp(&self) -> Self {
82 Self {
83 x: -self.y,
84 y: self.x,
85 }
86 }
87
88 pub fn add(&self, other: &Self) -> Self {
90 Self {
91 x: self.x + other.x,
92 y: self.y + other.y,
93 }
94 }
95
96 pub fn sub(&self, other: &Self) -> Self {
98 Self {
99 x: self.x - other.x,
100 y: self.y - other.y,
101 }
102 }
103
104 pub fn scale(&self, s: f64) -> Self {
106 Self {
107 x: self.x * s,
108 y: self.y * s,
109 }
110 }
111
112 pub fn lerp(&self, other: &Self, t: f64) -> Self {
114 Self {
115 x: self.x * (1.0 - t) + other.x * t,
116 y: self.y * (1.0 - t) + other.y * t,
117 }
118 }
119
120 pub fn angle(&self) -> f64 {
122 self.y.atan2(self.x)
123 }
124
125 pub fn from_polar(angle: f64, length: f64) -> Self {
127 Self {
128 x: length * angle.cos(),
129 y: length * angle.sin(),
130 }
131 }
132}
133
134pub fn signed_area(poly: &[Point2]) -> f64 {
140 let n = poly.len();
141 if n < 3 {
142 return 0.0;
143 }
144 let mut area = 0.0;
145 for i in 0..n {
146 let j = (i + 1) % n;
147 area += poly[i].x * poly[j].y - poly[j].x * poly[i].y;
148 }
149 area * 0.5
150}
151
152pub fn polygon_area(poly: &[Point2]) -> f64 {
154 signed_area(poly).abs()
155}
156
157pub fn polygon_perimeter(poly: &[Point2]) -> f64 {
159 let n = poly.len();
160 if n < 2 {
161 return 0.0;
162 }
163 let mut perim = 0.0;
164 for i in 0..n {
165 let j = (i + 1) % n;
166 perim += poly[i].distance(&poly[j]);
167 }
168 perim
169}
170
171pub fn polygon_centroid(poly: &[Point2]) -> Point2 {
173 let n = poly.len();
174 if n == 0 {
175 return Point2::zero();
176 }
177 let a = signed_area(poly);
178 if a.abs() < 1.0e-14 {
179 let mut cx = 0.0;
181 let mut cy = 0.0;
182 for p in poly {
183 cx += p.x;
184 cy += p.y;
185 }
186 return Point2::new(cx / n as f64, cy / n as f64);
187 }
188 let mut cx = 0.0;
189 let mut cy = 0.0;
190 for i in 0..n {
191 let j = (i + 1) % n;
192 let cross = poly[i].x * poly[j].y - poly[j].x * poly[i].y;
193 cx += (poly[i].x + poly[j].x) * cross;
194 cy += (poly[i].y + poly[j].y) * cross;
195 }
196 let factor = 1.0 / (6.0 * a);
197 Point2::new(cx * factor, cy * factor)
198}
199
200pub fn ensure_ccw(poly: &mut Vec<Point2>) {
202 if signed_area(poly) < 0.0 {
203 poly.reverse();
204 }
205}
206
207pub fn is_convex(poly: &[Point2]) -> bool {
209 let n = poly.len();
210 if n < 3 {
211 return true;
212 }
213 let mut sign = 0i32;
214 for i in 0..n {
215 let a = poly[i];
216 let b = poly[(i + 1) % n];
217 let c = poly[(i + 2) % n];
218 let cross = b.sub(&a).cross(&c.sub(&b));
219 if cross.abs() < 1.0e-12 {
220 continue;
221 }
222 let s = if cross > 0.0 { 1 } else { -1 };
223 if sign == 0 {
224 sign = s;
225 } else if sign != s {
226 return false;
227 }
228 }
229 true
230}
231
232pub fn point_in_polygon(pt: &Point2, poly: &[Point2]) -> bool {
234 let n = poly.len();
235 let mut inside = false;
236 let mut j = n - 1;
237 for i in 0..n {
238 let pi = &poly[i];
239 let pj = &poly[j];
240 if ((pi.y > pt.y) != (pj.y > pt.y))
241 && (pt.x < (pj.x - pi.x) * (pt.y - pi.y) / (pj.y - pi.y) + pi.x)
242 {
243 inside = !inside;
244 }
245 j = i;
246 }
247 inside
248}
249
250pub fn polygon_offset(poly: &[Point2], distance: f64) -> Vec<Point2> {
259 let n = poly.len();
260 if n < 3 {
261 return poly.to_vec();
262 }
263
264 let area = signed_area(poly);
266 let flip = if area < 0.0 { -1.0 } else { 1.0 };
267
268 let mut result = Vec::with_capacity(n);
269
270 for i in 0..n {
271 let prev = if i == 0 { n - 1 } else { i - 1 };
272 let next = (i + 1) % n;
273
274 let e_prev = poly[i].sub(&poly[prev]).normalized();
276 let e_next = poly[next].sub(&poly[i]).normalized();
277
278 let n_prev = Point2::new(e_prev.y * flip, -e_prev.x * flip);
280 let n_next = Point2::new(e_next.y * flip, -e_next.x * flip);
281
282 let bisector = n_prev.add(&n_next);
284 let bl = bisector.length();
285 if bl < 1.0e-14 {
286 result.push(poly[i].add(&n_prev.scale(distance)));
288 continue;
289 }
290 let bisector = bisector.scale(1.0 / bl);
291
292 let cos_half = bisector.dot(&n_prev).max(0.1); let offset_len = distance / cos_half;
295
296 result.push(poly[i].add(&bisector.scale(offset_len)));
297 }
298
299 result
300}
301
302pub fn polygon_offset_round(
306 poly: &[Point2],
307 distance: f64,
308 segments_per_corner: usize,
309) -> Vec<Point2> {
310 let n = poly.len();
311 if n < 3 {
312 return poly.to_vec();
313 }
314
315 let area = signed_area(poly);
316 let flip = if area < 0.0 { -1.0 } else { 1.0 };
317 let segs = segments_per_corner.max(1);
318
319 let mut result = Vec::new();
320
321 for i in 0..n {
322 let prev = if i == 0 { n - 1 } else { i - 1 };
323 let next = (i + 1) % n;
324
325 let e_prev = poly[i].sub(&poly[prev]).normalized();
326 let e_next = poly[next].sub(&poly[i]).normalized();
327
328 let n_prev = Point2::new(e_prev.y * flip, -e_prev.x * flip);
329 let n_next = Point2::new(e_next.y * flip, -e_next.x * flip);
330
331 let cross = e_prev.cross(&e_next) * flip;
332
333 if (distance > 0.0 && cross < -1.0e-10) || (distance < 0.0 && cross > 1.0e-10) {
334 let angle_start = n_prev.angle();
336 let mut angle_end = n_next.angle();
337 if distance > 0.0 {
338 while angle_end < angle_start {
339 angle_end += 2.0 * PI;
340 }
341 } else {
342 while angle_end > angle_start {
343 angle_end -= 2.0 * PI;
344 }
345 }
346 for k in 0..=segs {
347 let t = k as f64 / segs as f64;
348 let angle = angle_start + t * (angle_end - angle_start);
349 let offset = Point2::from_polar(angle, distance.abs());
350 let sign = if distance > 0.0 { 1.0 } else { -1.0 };
351 result.push(poly[i].add(&offset.scale(sign)));
352 }
353 } else {
354 let bisector = n_prev.add(&n_next);
356 let bl = bisector.length();
357 if bl < 1.0e-14 {
358 result.push(poly[i].add(&n_prev.scale(distance)));
359 } else {
360 let bisector = bisector.scale(1.0 / bl);
361 let cos_half = bisector.dot(&n_prev).max(0.1);
362 let offset_len = distance / cos_half;
363 result.push(poly[i].add(&bisector.scale(offset_len)));
364 }
365 }
366 }
367
368 result
369}
370
371pub fn polyline_offset(polyline: &[Point2], distance: f64) -> Vec<Point2> {
379 let n = polyline.len();
380 if n < 2 {
381 return polyline.to_vec();
382 }
383 if n == 2 {
384 let e = polyline[1].sub(&polyline[0]).normalized();
385 let normal = Point2::new(-e.y, e.x);
386 let offset = normal.scale(distance);
387 return vec![polyline[0].add(&offset), polyline[1].add(&offset)];
388 }
389
390 let mut result = Vec::with_capacity(n);
391
392 let e0 = polyline[1].sub(&polyline[0]).normalized();
394 let n0 = Point2::new(-e0.y, e0.x);
395 result.push(polyline[0].add(&n0.scale(distance)));
396
397 for i in 1..n - 1 {
399 let e_prev = polyline[i].sub(&polyline[i - 1]).normalized();
400 let e_next = polyline[i + 1].sub(&polyline[i]).normalized();
401 let n_prev = Point2::new(-e_prev.y, e_prev.x);
402 let n_next = Point2::new(-e_next.y, e_next.x);
403 let bisector = n_prev.add(&n_next);
404 let bl = bisector.length();
405 if bl < 1.0e-14 {
406 result.push(polyline[i].add(&n_prev.scale(distance)));
407 } else {
408 let bisector = bisector.scale(1.0 / bl);
409 let cos_half = bisector.dot(&n_prev).max(0.1);
410 let offset_len = distance / cos_half;
411 result.push(polyline[i].add(&bisector.scale(offset_len)));
412 }
413 }
414
415 let e_last = polyline[n - 1].sub(&polyline[n - 2]).normalized();
417 let n_last = Point2::new(-e_last.y, e_last.x);
418 result.push(polyline[n - 1].add(&n_last.scale(distance)));
419
420 result
421}
422
423pub fn fillet_polygon(poly: &[Point2], radius: f64, segments: usize) -> Vec<Point2> {
431 let n = poly.len();
432 if n < 3 || radius <= 0.0 {
433 return poly.to_vec();
434 }
435 let segs = segments.max(2);
436 let mut result = Vec::new();
437
438 for i in 0..n {
439 let prev = if i == 0 { n - 1 } else { i - 1 };
440 let next = (i + 1) % n;
441
442 let to_prev = poly[prev].sub(&poly[i]).normalized();
443 let to_next = poly[next].sub(&poly[i]).normalized();
444
445 let dot = to_prev.dot(&to_next).clamp(-1.0, 1.0);
446 let half_angle = ((1.0 + dot) / 2.0).sqrt().acos().max(1.0e-6);
447 let tan_half = half_angle.tan().max(1.0e-6);
448 let trim = (radius / tan_half).min(
449 poly[i]
450 .distance(&poly[prev])
451 .min(poly[i].distance(&poly[next]))
452 * 0.49,
453 );
454 let actual_radius = trim * tan_half;
455
456 let p_start = poly[i].add(&to_prev.scale(trim));
458 let p_end = poly[i].add(&to_next.scale(trim));
459
460 let bisector = to_prev.add(&to_next).normalized();
462 let center_dist = actual_radius / half_angle.sin().max(1.0e-6);
463 let center = poly[i].add(&bisector.scale(center_dist));
464
465 let angle_start = p_start.sub(¢er).angle();
467 let angle_end = p_end.sub(¢er).angle();
468 let mut sweep = angle_end - angle_start;
469 while sweep > PI {
471 sweep -= 2.0 * PI;
472 }
473 while sweep < -PI {
474 sweep += 2.0 * PI;
475 }
476
477 for k in 0..=segs {
478 let t = k as f64 / segs as f64;
479 let a = angle_start + t * sweep;
480 result.push(Point2::new(
481 center.x + actual_radius * a.cos(),
482 center.y + actual_radius * a.sin(),
483 ));
484 }
485 }
486
487 result
488}
489
490pub fn chamfer_polygon(poly: &[Point2], distance: f64) -> Vec<Point2> {
494 let n = poly.len();
495 if n < 3 || distance <= 0.0 {
496 return poly.to_vec();
497 }
498 let mut result = Vec::new();
499
500 for i in 0..n {
501 let prev = if i == 0 { n - 1 } else { i - 1 };
502 let next = (i + 1) % n;
503
504 let to_prev = poly[prev].sub(&poly[i]).normalized();
505 let to_next = poly[next].sub(&poly[i]).normalized();
506
507 let d_prev = poly[i].distance(&poly[prev]);
508 let d_next = poly[i].distance(&poly[next]);
509 let trim = distance.min(d_prev * 0.49).min(d_next * 0.49);
510
511 let p1 = poly[i].add(&to_prev.scale(trim));
512 let p2 = poly[i].add(&to_next.scale(trim));
513
514 result.push(p1);
515 result.push(p2);
516 }
517
518 result
519}
520
521pub fn rounded_rectangle(
530 width: f64,
531 height: f64,
532 radius: f64,
533 segments_per_corner: usize,
534) -> Vec<Point2> {
535 let r = radius.min(width / 2.0).min(height / 2.0).max(0.0);
536 let hw = width / 2.0;
537 let hh = height / 2.0;
538 let segs = segments_per_corner.max(1);
539
540 let mut result = Vec::new();
541
542 let corners = [
544 (hw - r, hh - r, 0.0),
545 (-(hw - r), hh - r, PI / 2.0),
546 (-(hw - r), -(hh - r), PI),
547 (hw - r, -(hh - r), 3.0 * PI / 2.0),
548 ];
549
550 for &(cx, cy, start_angle) in &corners {
551 for k in 0..=segs {
552 let t = k as f64 / segs as f64;
553 let a = start_angle + t * (PI / 2.0);
554 result.push(Point2::new(cx + r * a.cos(), cy + r * a.sin()));
555 }
556 }
557
558 result
559}
560
561pub fn minkowski_sum_convex(a: &[Point2], b: &[Point2]) -> Vec<Point2> {
569 if a.is_empty() || b.is_empty() {
570 return Vec::new();
571 }
572 let na = a.len();
573 let nb = b.len();
574
575 let mut ia = 0;
577 for i in 1..na {
578 if a[i].y < a[ia].y || (a[i].y == a[ia].y && a[i].x < a[ia].x) {
579 ia = i;
580 }
581 }
582 let mut ib = 0;
583 for i in 1..nb {
584 if b[i].y < b[ib].y || (b[i].y == b[ib].y && b[i].x < b[ib].x) {
585 ib = i;
586 }
587 }
588
589 let mut result = Vec::with_capacity(na + nb);
590 let mut i = 0;
591 let mut j = 0;
592
593 while i < na || j < nb {
594 let ai = (ia + i) % na;
595 let bi = (ib + j) % nb;
596 result.push(a[ai].add(&b[bi]));
597
598 let a_next = (ia + i + 1) % na;
599 let b_next = (ib + j + 1) % nb;
600
601 let ea = a[a_next].sub(&a[ai]);
602 let eb = b[b_next].sub(&b[bi]);
603
604 let cross = ea.cross(&eb);
605
606 if i >= na {
607 j += 1;
608 } else if j >= nb {
609 i += 1;
610 } else if cross > 0.0 {
611 i += 1;
612 } else if cross < 0.0 {
613 j += 1;
614 } else {
615 i += 1;
617 j += 1;
618 }
619 }
620
621 result
622}
623
624pub fn minkowski_difference_convex(a: &[Point2], b: &[Point2]) -> Vec<Point2> {
628 let b_reflected: Vec<Point2> = b.iter().map(|p| Point2::new(-p.x, -p.y)).collect();
629 let mut b_ref = b_reflected;
631 ensure_ccw(&mut b_ref);
632 minkowski_sum_convex(a, &b_ref)
633}
634
635pub fn morphological_dilation(poly: &[Point2], radius: f64) -> Vec<Point2> {
641 polygon_offset(poly, radius.abs())
642}
643
644pub fn morphological_erosion(poly: &[Point2], radius: f64) -> Vec<Point2> {
646 polygon_offset(poly, -radius.abs())
647}
648
649pub fn morphological_opening(poly: &[Point2], radius: f64) -> Vec<Point2> {
653 let eroded = morphological_erosion(poly, radius);
654 morphological_dilation(&eroded, radius)
655}
656
657pub fn morphological_closing(poly: &[Point2], radius: f64) -> Vec<Point2> {
661 let dilated = morphological_dilation(poly, radius);
662 morphological_erosion(&dilated, radius)
663}
664
665pub fn tubular_neighborhood(polyline: &[Point2], radius: f64) -> Vec<Point2> {
674 if polyline.len() < 2 {
675 if polyline.len() == 1 {
676 let mut circle = Vec::new();
678 let segs = 16;
679 for k in 0..segs {
680 let a = 2.0 * PI * k as f64 / segs as f64;
681 circle.push(Point2::new(
682 polyline[0].x + radius * a.cos(),
683 polyline[0].y + radius * a.sin(),
684 ));
685 }
686 return circle;
687 }
688 return Vec::new();
689 }
690
691 let left = polyline_offset(polyline, radius);
692 let right = polyline_offset(polyline, -radius);
693
694 let mut result = Vec::new();
696
697 let e0 = polyline[1].sub(&polyline[0]).normalized();
699 let n0 = Point2::new(-e0.y, e0.x);
700 let angle_start = n0.scale(radius).add(&polyline[0]).sub(&polyline[0]).angle();
701 let cap_segs = 8;
702 result.extend_from_slice(&left);
704
705 let n = polyline.len();
707 let e_end = polyline[n - 1].sub(&polyline[n - 2]).normalized();
708 let n_end = Point2::new(-e_end.y, e_end.x);
709 let cap_start = n_end.angle();
710 for k in 0..=cap_segs {
711 let t = k as f64 / cap_segs as f64;
712 let a = cap_start - t * PI;
713 result.push(Point2::new(
714 polyline[n - 1].x + radius * a.cos(),
715 polyline[n - 1].y + radius * a.sin(),
716 ));
717 }
718
719 for p in right.iter().rev() {
721 result.push(*p);
722 }
723
724 let _n0_angle = Point2::new(e0.y, -e0.x).angle();
726 let cap_start2 = Point2::new(e0.y, -e0.x).angle();
727 for k in 0..=cap_segs {
728 let t = k as f64 / cap_segs as f64;
729 let a = cap_start2 - t * PI;
730 result.push(Point2::new(
731 polyline[0].x + radius * a.cos(),
732 polyline[0].y + radius * a.sin(),
733 ));
734 }
735
736 let _ = angle_start; result
738}
739
740pub fn point_to_segment_distance(pt: &Point2, a: &Point2, b: &Point2) -> f64 {
742 let ab = b.sub(a);
743 let ap = pt.sub(a);
744 let t = (ap.dot(&ab) / ab.dot(&ab)).clamp(0.0, 1.0);
745 let proj = a.add(&ab.scale(t));
746 pt.distance(&proj)
747}
748
749pub fn point_in_tubular(pt: &Point2, polyline: &[Point2], radius: f64) -> bool {
751 for i in 0..polyline.len().saturating_sub(1) {
752 if point_to_segment_distance(pt, &polyline[i], &polyline[i + 1]) <= radius {
753 return true;
754 }
755 }
756 false
757}
758
759pub fn polygon_offset_3d(
768 vertices: &[[f64; 3]],
769 distance: f64,
770 plane_normal: [f64; 3],
771) -> Vec<[f64; 3]> {
772 if vertices.len() < 3 {
773 return vertices.to_vec();
774 }
775
776 let n = normalize_3d(plane_normal);
778 let u = find_perpendicular(n);
779 let v = cross_3d(n, u);
780
781 let origin = vertices[0];
783 let poly2d: Vec<Point2> = vertices
784 .iter()
785 .map(|p| {
786 let rel = [p[0] - origin[0], p[1] - origin[1], p[2] - origin[2]];
787 Point2::new(dot_3d(rel, u), dot_3d(rel, v))
788 })
789 .collect();
790
791 let offset2d = polygon_offset(&poly2d, distance);
793
794 offset2d
796 .iter()
797 .map(|p| {
798 [
799 origin[0] + p.x * u[0] + p.y * v[0],
800 origin[1] + p.x * u[1] + p.y * v[1],
801 origin[2] + p.x * u[2] + p.y * v[2],
802 ]
803 })
804 .collect()
805}
806
807fn normalize_3d(v: [f64; 3]) -> [f64; 3] {
809 let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
810 if l < 1.0e-14 {
811 return [0.0, 0.0, 1.0];
812 }
813 [v[0] / l, v[1] / l, v[2] / l]
814}
815
816fn dot_3d(a: [f64; 3], b: [f64; 3]) -> f64 {
818 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
819}
820
821fn cross_3d(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
823 [
824 a[1] * b[2] - a[2] * b[1],
825 a[2] * b[0] - a[0] * b[2],
826 a[0] * b[1] - a[1] * b[0],
827 ]
828}
829
830fn find_perpendicular(n: [f64; 3]) -> [f64; 3] {
832 let candidate = if n[0].abs() < 0.9 {
833 [1.0, 0.0, 0.0]
834 } else {
835 [0.0, 1.0, 0.0]
836 };
837 let c = cross_3d(n, candidate);
838 normalize_3d(c)
839}
840
841#[cfg(test)]
846mod tests {
847 use super::*;
848
849 const EPS: f64 = 1e-10;
850
851 fn square() -> Vec<Point2> {
852 vec![
853 Point2::new(0.0, 0.0),
854 Point2::new(1.0, 0.0),
855 Point2::new(1.0, 1.0),
856 Point2::new(0.0, 1.0),
857 ]
858 }
859
860 fn triangle() -> Vec<Point2> {
861 vec![
862 Point2::new(0.0, 0.0),
863 Point2::new(2.0, 0.0),
864 Point2::new(1.0, 1.732),
865 ]
866 }
867
868 #[test]
870 fn test_signed_area_ccw() {
871 let sq = square();
872 let a = signed_area(&sq);
873 assert!((a - 1.0).abs() < 1.0e-6, "Area should be 1.0, got {:.6}", a);
874 }
875
876 #[test]
878 fn test_signed_area_cw() {
879 let mut sq = square();
880 sq.reverse();
881 let a = signed_area(&sq);
882 assert!(
883 (a - (-1.0)).abs() < 1.0e-6,
884 "CW area should be -1.0, got {:.6}",
885 a
886 );
887 }
888
889 #[test]
891 fn test_polygon_area() {
892 let mut sq = square();
893 sq.reverse();
894 assert!((polygon_area(&sq) - 1.0).abs() < 1.0e-6);
895 }
896
897 #[test]
899 fn test_perimeter() {
900 let sq = square();
901 let p = polygon_perimeter(&sq);
902 assert!(
903 (p - 4.0).abs() < 1.0e-6,
904 "Perimeter should be 4.0, got {:.6}",
905 p
906 );
907 }
908
909 #[test]
911 fn test_centroid() {
912 let sq = square();
913 let c = polygon_centroid(&sq);
914 assert!(
915 (c.x - 0.5).abs() < 1.0e-6,
916 "cx should be 0.5, got {:.6}",
917 c.x
918 );
919 assert!(
920 (c.y - 0.5).abs() < 1.0e-6,
921 "cy should be 0.5, got {:.6}",
922 c.y
923 );
924 }
925
926 #[test]
928 fn test_square_convex() {
929 assert!(is_convex(&square()));
930 }
931
932 #[test]
934 fn test_l_shape_not_convex() {
935 let l = vec![
936 Point2::new(0.0, 0.0),
937 Point2::new(2.0, 0.0),
938 Point2::new(2.0, 1.0),
939 Point2::new(1.0, 1.0),
940 Point2::new(1.0, 2.0),
941 Point2::new(0.0, 2.0),
942 ];
943 assert!(!is_convex(&l));
944 }
945
946 #[test]
948 fn test_point_inside() {
949 let sq = square();
950 assert!(point_in_polygon(&Point2::new(0.5, 0.5), &sq));
951 }
952
953 #[test]
955 fn test_point_outside() {
956 let sq = square();
957 assert!(!point_in_polygon(&Point2::new(2.0, 2.0), &sq));
958 }
959
960 #[test]
962 fn test_offset_increases_area() {
963 let sq = square();
964 let offset = polygon_offset(&sq, 0.1);
965 let area_orig = polygon_area(&sq);
966 let area_offset = polygon_area(&offset);
967 assert!(
968 area_offset > area_orig,
969 "Offset area {:.6} should be > original {:.6}",
970 area_offset,
971 area_orig
972 );
973 }
974
975 #[test]
977 fn test_offset_decreases_area() {
978 let sq = square();
979 let offset = polygon_offset(&sq, -0.1);
980 let area_orig = polygon_area(&sq);
981 let area_offset = polygon_area(&offset);
982 assert!(
983 area_offset < area_orig,
984 "Inward offset area {:.6} should be < original {:.6}",
985 area_offset,
986 area_orig
987 );
988 }
989
990 #[test]
992 fn test_zero_offset() {
993 let sq = square();
994 let offset = polygon_offset(&sq, 0.0);
995 assert_eq!(offset.len(), sq.len());
996 for (a, b) in offset.iter().zip(sq.iter()) {
997 assert!(a.distance(b) < 1.0e-6);
998 }
999 }
1000
1001 #[test]
1003 fn test_polyline_offset_count() {
1004 let line = vec![
1005 Point2::new(0.0, 0.0),
1006 Point2::new(1.0, 0.0),
1007 Point2::new(2.0, 1.0),
1008 ];
1009 let offset = polyline_offset(&line, 0.1);
1010 assert_eq!(offset.len(), line.len());
1011 }
1012
1013 #[test]
1015 fn test_fillet_more_points() {
1016 let sq = square();
1017 let filleted = fillet_polygon(&sq, 0.1, 4);
1018 assert!(
1019 filleted.len() > sq.len(),
1020 "Fillet should add points: {} vs {}",
1021 filleted.len(),
1022 sq.len()
1023 );
1024 }
1025
1026 #[test]
1028 fn test_chamfer_doubles() {
1029 let sq = square();
1030 let chamfered = chamfer_polygon(&sq, 0.1);
1031 assert_eq!(
1032 chamfered.len(),
1033 2 * sq.len(),
1034 "Chamfer should produce 2 points per vertex"
1035 );
1036 }
1037
1038 #[test]
1040 fn test_rounded_rect_points() {
1041 let rect = rounded_rectangle(4.0, 2.0, 0.5, 4);
1042 assert_eq!(rect.len(), 20, "Should have 20 points, got {}", rect.len());
1044 }
1045
1046 #[test]
1048 fn test_rounded_rect_area() {
1049 let rect = rounded_rectangle(4.0, 2.0, 0.5, 16);
1050 let area = polygon_area(&rect);
1051 let rect_area = 4.0 * 2.0;
1052 assert!(
1054 area < rect_area + 0.1 && area > rect_area - 2.0,
1055 "Rounded rect area {:.6} should be near {:.6}",
1056 area,
1057 rect_area
1058 );
1059 }
1060
1061 #[test]
1063 fn test_minkowski_sum_squares() {
1064 let a = square();
1065 let b = vec![
1066 Point2::new(0.0, 0.0),
1067 Point2::new(0.5, 0.0),
1068 Point2::new(0.5, 0.5),
1069 Point2::new(0.0, 0.5),
1070 ];
1071 let sum = minkowski_sum_convex(&a, &b);
1072 assert!(!sum.is_empty(), "Minkowski sum should not be empty");
1073 let area = polygon_area(&sum);
1074 assert!(
1076 (area - 2.25).abs() < 0.5,
1077 "Minkowski sum area should be ~2.25, got {:.6}",
1078 area
1079 );
1080 }
1081
1082 #[test]
1084 fn test_minkowski_difference() {
1085 let a = vec![
1086 Point2::new(0.0, 0.0),
1087 Point2::new(2.0, 0.0),
1088 Point2::new(2.0, 2.0),
1089 Point2::new(0.0, 2.0),
1090 ];
1091 let b = vec![
1092 Point2::new(0.0, 0.0),
1093 Point2::new(0.5, 0.0),
1094 Point2::new(0.5, 0.5),
1095 Point2::new(0.0, 0.5),
1096 ];
1097 let diff = minkowski_difference_convex(&a, &b);
1098 assert!(!diff.is_empty(), "Minkowski difference should not be empty");
1099 }
1100
1101 #[test]
1103 fn test_dilation_area() {
1104 let sq = square();
1105 let dilated = morphological_dilation(&sq, 0.2);
1106 assert!(polygon_area(&dilated) > polygon_area(&sq));
1107 }
1108
1109 #[test]
1111 fn test_erosion_area() {
1112 let sq = square();
1113 let eroded = morphological_erosion(&sq, 0.1);
1114 assert!(
1115 polygon_area(&eroded) < polygon_area(&sq),
1116 "Erosion should reduce area"
1117 );
1118 }
1119
1120 #[test]
1122 fn test_opening_area() {
1123 let sq = square();
1124 let opened = morphological_opening(&sq, 0.05);
1125 assert!(polygon_area(&opened) <= polygon_area(&sq) + 1.0e-3);
1126 }
1127
1128 #[test]
1130 fn test_closing_area() {
1131 let sq = square();
1132 let closed = morphological_closing(&sq, 0.05);
1133 assert!(polygon_area(&closed) >= polygon_area(&sq) - 1.0e-3);
1135 }
1136
1137 #[test]
1139 fn test_tubular_closed() {
1140 let line = vec![Point2::new(0.0, 0.0), Point2::new(3.0, 0.0)];
1141 let tube = tubular_neighborhood(&line, 0.5);
1142 assert!(tube.len() >= 4, "Tube should have at least 4 points");
1143 }
1144
1145 #[test]
1147 fn test_point_segment_dist() {
1148 let a = Point2::new(0.0, 0.0);
1149 let b = Point2::new(2.0, 0.0);
1150 let pt = Point2::new(1.0, 1.0);
1151 let d = point_to_segment_distance(&pt, &a, &b);
1152 assert!(
1153 (d - 1.0).abs() < 1.0e-6,
1154 "Distance should be 1.0, got {:.6}",
1155 d
1156 );
1157 }
1158
1159 #[test]
1161 fn test_point_in_tubular() {
1162 let line = vec![Point2::new(0.0, 0.0), Point2::new(2.0, 0.0)];
1163 assert!(point_in_tubular(&Point2::new(1.0, 0.3), &line, 0.5));
1164 assert!(!point_in_tubular(&Point2::new(1.0, 1.0), &line, 0.5));
1165 }
1166
1167 #[test]
1169 fn test_round_offset_more_points() {
1170 let sq = square();
1171 let miter = polygon_offset(&sq, 0.2);
1172 let round = polygon_offset_round(&sq, 0.2, 4);
1173 assert!(
1174 round.len() >= miter.len(),
1175 "Round offset should have >= miter points: {} vs {}",
1176 round.len(),
1177 miter.len()
1178 );
1179 }
1180
1181 #[test]
1183 fn test_ensure_ccw() {
1184 let mut poly = square();
1185 poly.reverse();
1186 assert!(signed_area(&poly) < 0.0);
1187 ensure_ccw(&mut poly);
1188 assert!(signed_area(&poly) > 0.0);
1189 }
1190
1191 #[test]
1193 fn test_3d_offset_count() {
1194 let verts = vec![
1195 [0.0, 0.0, 0.0],
1196 [1.0, 0.0, 0.0],
1197 [1.0, 1.0, 0.0],
1198 [0.0, 1.0, 0.0],
1199 ];
1200 let offset = polygon_offset_3d(&verts, 0.1, [0.0, 0.0, 1.0]);
1201 assert_eq!(offset.len(), verts.len());
1202 }
1203
1204 #[test]
1206 fn test_point2_distance() {
1207 let a = Point2::new(0.0, 0.0);
1208 let b = Point2::new(3.0, 4.0);
1209 assert!((a.distance(&b) - 5.0).abs() < EPS);
1210 }
1211
1212 #[test]
1214 fn test_point2_normalized() {
1215 let p = Point2::new(3.0, 4.0);
1216 let n = p.normalized();
1217 assert!((n.length() - 1.0).abs() < EPS);
1218 }
1219
1220 #[test]
1222 fn test_triangle_area() {
1223 let tri = triangle();
1224 let a = polygon_area(&tri);
1225 assert!(
1227 a > 1.5 && a < 2.0,
1228 "Triangle area should be ~1.732, got {:.6}",
1229 a
1230 );
1231 }
1232}