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