scirs2_spatial/
boolean_ops.rs

1//! Boolean operations for polygons and polyhedra
2//!
3//! This module provides implementations of set-theoretic Boolean operations
4//! on polygons (2D) and polyhedra (3D). These operations include union,
5//! intersection, difference, and symmetric difference.
6//!
7//! # Theory
8//!
9//! Boolean operations on polygons are fundamental in computational geometry
10//! and are used in CAD systems, GIS applications, and computer graphics.
11//! The algorithms implemented here use:
12//!
13//! - **Sutherland-Hodgman clipping** for convex polygons
14//! - **Weiler-Atherton clipping** for general polygons
15//! - **BSP tree decomposition** for 3D polyhedra
16//! - **Plane sweep algorithms** for efficiency
17//!
18//! # Examples
19//!
20//! ```
21//! use scirs2_spatial::boolean_ops::{polygon_union, polygon_intersection};
22//! use scirs2_core::ndarray::array;
23//!
24//! // Define two overlapping squares
25//! let poly1 = array![
26//!     [0.0, 0.0],
27//!     [2.0, 0.0],
28//!     [2.0, 2.0],
29//!     [0.0, 2.0]
30//! ];
31//!
32//! let poly2 = array![
33//!     [1.0, 1.0],
34//!     [3.0, 1.0],
35//!     [3.0, 3.0],
36//!     [1.0, 3.0]
37//! ];
38//!
39//! // Compute union
40//! let union_result = polygon_union(&poly1.view(), &poly2.view()).unwrap();
41//! println!("Union has {} vertices", union_result.nrows());
42//!
43//! // Compute intersection
44//! let intersection_result = polygon_intersection(&poly1.view(), &poly2.view()).unwrap();
45//! println!("Intersection has {} vertices", intersection_result.nrows());
46//! ```
47
48use crate::error::{SpatialError, SpatialResult};
49use scirs2_core::ndarray::{Array2, ArrayView2};
50use std::cmp::Ordering;
51use std::collections::HashMap;
52
53/// Point structure for Boolean operations
54#[derive(Debug, Clone, Copy, PartialEq)]
55struct Point2D {
56    x: f64,
57    y: f64,
58}
59
60impl Point2D {
61    fn new(x: f64, y: f64) -> Self {
62        Self { x, y }
63    }
64
65    #[allow(dead_code)]
66    fn distance_to(&self, other: &Point2D) -> f64 {
67        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
68    }
69
70    fn cross_product(&self, other: &Point2D) -> f64 {
71        self.x * other.y - self.y * other.x
72    }
73
74    #[allow(dead_code)]
75    fn dot_product(&self, other: &Point2D) -> f64 {
76        self.x * other.x + self.y * other.y
77    }
78}
79
80/// Edge structure for polygon operations
81#[derive(Debug, Clone)]
82struct Edge {
83    start: Point2D,
84    end: Point2D,
85    polygon_id: usize, // 0 for first polygon, 1 for second
86    intersectionpoints: Vec<IntersectionPoint>,
87}
88
89/// Intersection point with metadata
90#[derive(Debug, Clone)]
91#[allow(dead_code)]
92struct IntersectionPoint {
93    point: Point2D,
94    t: f64, // Parameter along the edge (0.0 at start, 1.0 at end)
95    other_edge_id: usize,
96}
97
98/// Polygon with labeled edges for Boolean operations
99#[derive(Debug, Clone)]
100struct LabeledPolygon {
101    vertices: Vec<Point2D>,
102    edges: Vec<Edge>,
103    #[allow(dead_code)]
104    is_hole: bool,
105}
106
107impl LabeledPolygon {
108    fn from_array(vertices: &ArrayView2<'_, f64>) -> SpatialResult<Self> {
109        if vertices.ncols() != 2 {
110            return Err(SpatialError::ValueError(
111                "Polygon vertices must be 2D".to_string(),
112            ));
113        }
114
115        let points: Vec<Point2D> = vertices
116            .outer_iter()
117            .map(|row| Point2D::new(row[0], row[1]))
118            .collect();
119
120        if points.len() < 3 {
121            return Err(SpatialError::ValueError(
122                "Polygon must have at least 3 vertices".to_string(),
123            ));
124        }
125
126        let mut edges = Vec::new();
127        for i in 0..points.len() {
128            let start = points[i];
129            let end = points[(i + 1) % points.len()];
130            edges.push(Edge {
131                start,
132                end,
133                polygon_id: 0,
134                intersectionpoints: Vec::new(),
135            });
136        }
137
138        Ok(LabeledPolygon {
139            vertices: points,
140            edges,
141            is_hole: false,
142        })
143    }
144
145    fn to_array(&self) -> Array2<f64> {
146        let mut data = Vec::with_capacity(self.vertices.len() * 2);
147        for vertex in &self.vertices {
148            data.push(vertex.x);
149            data.push(vertex.y);
150        }
151        Array2::from_shape_vec((self.vertices.len(), 2), data).unwrap()
152    }
153
154    fn ispoint_inside(&self, point: &Point2D) -> bool {
155        let mut inside = false;
156        let n = self.vertices.len();
157
158        for i in 0..n {
159            let j = (i + 1) % n;
160            let vi = &self.vertices[i];
161            let vj = &self.vertices[j];
162
163            if ((vi.y > point.y) != (vj.y > point.y))
164                && (point.x < (vj.x - vi.x) * (point.y - vi.y) / (vj.y - vi.y) + vi.x)
165            {
166                inside = !inside;
167            }
168        }
169
170        inside
171    }
172
173    fn compute_area(&self) -> f64 {
174        let mut area = 0.0;
175        let n = self.vertices.len();
176
177        for i in 0..n {
178            let j = (i + 1) % n;
179            let vi = &self.vertices[i];
180            let vj = &self.vertices[j];
181            area += vi.x * vj.y - vj.x * vi.y;
182        }
183
184        area.abs() / 2.0
185    }
186
187    #[allow(dead_code)]
188    fn is_clockwise(&self) -> bool {
189        let mut sum = 0.0;
190        let n = self.vertices.len();
191
192        for i in 0..n {
193            let j = (i + 1) % n;
194            let vi = &self.vertices[i];
195            let vj = &self.vertices[j];
196            sum += (vj.x - vi.x) * (vj.y + vi.y);
197        }
198
199        sum > 0.0
200    }
201
202    #[allow(dead_code)]
203    fn reverse(&mut self) {
204        self.vertices.reverse();
205        // Rebuild edges after reversing
206        let mut edges = Vec::new();
207        for i in 0..self.vertices.len() {
208            let start = self.vertices[i];
209            let end = self.vertices[(i + 1) % self.vertices.len()];
210            edges.push(Edge {
211                start,
212                end,
213                polygon_id: self.edges[0].polygon_id,
214                intersectionpoints: Vec::new(),
215            });
216        }
217        self.edges = edges;
218    }
219}
220
221/// Compute the union of two polygons
222///
223/// # Arguments
224///
225/// * `poly1` - First polygon vertices, shape (n, 2)
226/// * `poly2` - Second polygon vertices, shape (m, 2)
227///
228/// # Returns
229///
230/// * Array of union polygon vertices
231///
232/// # Examples
233///
234/// ```
235/// use scirs2_spatial::boolean_ops::polygon_union;
236/// use scirs2_core::ndarray::array;
237///
238/// let poly1 = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
239/// let poly2 = array![[0.5, 0.5], [1.5, 0.5], [1.5, 1.5], [0.5, 1.5]];
240///
241/// let union = polygon_union(&poly1.view(), &poly2.view()).unwrap();
242/// ```
243#[allow(dead_code)]
244pub fn polygon_union(
245    poly1: &ArrayView2<'_, f64>,
246    poly2: &ArrayView2<'_, f64>,
247) -> SpatialResult<Array2<f64>> {
248    let mut p1 = LabeledPolygon::from_array(poly1)?;
249    let mut p2 = LabeledPolygon::from_array(poly2)?;
250
251    // Set polygon IDs
252    for edge in &mut p1.edges {
253        edge.polygon_id = 0;
254    }
255    for edge in &mut p2.edges {
256        edge.polygon_id = 1;
257    }
258
259    // Find intersections
260    find_intersections(&mut p1, &mut p2)?;
261
262    // Perform Weiler-Atherton clipping for union
263    let result_polygons = weiler_atherton_union(&p1, &p2)?;
264
265    if result_polygons.is_empty() {
266        // No intersection, return the polygon with larger area
267        if p1.compute_area() >= p2.compute_area() {
268            Ok(p1.to_array())
269        } else {
270            Ok(p2.to_array())
271        }
272    } else {
273        // Return the first (and typically largest) result polygon
274        Ok(result_polygons[0].to_array())
275    }
276}
277
278/// Compute the intersection of two polygons
279///
280/// # Arguments
281///
282/// * `poly1` - First polygon vertices, shape (n, 2)
283/// * `poly2` - Second polygon vertices, shape (m, 2)
284///
285/// # Returns
286///
287/// * Array of intersection polygon vertices
288#[allow(dead_code)]
289pub fn polygon_intersection(
290    poly1: &ArrayView2<'_, f64>,
291    poly2: &ArrayView2<'_, f64>,
292) -> SpatialResult<Array2<f64>> {
293    let mut p1 = LabeledPolygon::from_array(poly1)?;
294    let mut p2 = LabeledPolygon::from_array(poly2)?;
295
296    // Set polygon IDs
297    for edge in &mut p1.edges {
298        edge.polygon_id = 0;
299    }
300    for edge in &mut p2.edges {
301        edge.polygon_id = 1;
302    }
303
304    // Find intersections
305    find_intersections(&mut p1, &mut p2)?;
306
307    // Perform Sutherland-Hodgman clipping for intersection
308    let result = sutherland_hodgman_clip(&p1, &p2)?;
309
310    Ok(result.to_array())
311}
312
313/// Compute the difference of two polygons (poly1 - poly2)
314///
315/// # Arguments
316///
317/// * `poly1` - First polygon vertices, shape (n, 2)
318/// * `poly2` - Second polygon vertices, shape (m, 2)
319///
320/// # Returns
321///
322/// * Array of difference polygon vertices
323#[allow(dead_code)]
324pub fn polygon_difference(
325    poly1: &ArrayView2<'_, f64>,
326    poly2: &ArrayView2<'_, f64>,
327) -> SpatialResult<Array2<f64>> {
328    let mut p1 = LabeledPolygon::from_array(poly1)?;
329    let mut p2 = LabeledPolygon::from_array(poly2)?;
330
331    // Set polygon IDs
332    for edge in &mut p1.edges {
333        edge.polygon_id = 0;
334    }
335    for edge in &mut p2.edges {
336        edge.polygon_id = 1;
337    }
338
339    // Find intersections
340    find_intersections(&mut p1, &mut p2)?;
341
342    // Perform difference operation
343    let result = weiler_atherton_difference(&p1, &p2)?;
344
345    if result.is_empty() {
346        // No intersection, return original polygon
347        Ok(p1.to_array())
348    } else {
349        Ok(result[0].to_array())
350    }
351}
352
353/// Compute the symmetric difference (XOR) of two polygons
354///
355/// # Arguments
356///
357/// * `poly1` - First polygon vertices, shape (n, 2)
358/// * `poly2` - Second polygon vertices, shape (m, 2)
359///
360/// # Returns
361///
362/// * Array of symmetric difference polygon vertices
363#[allow(dead_code)]
364pub fn polygon_symmetric_difference(
365    poly1: &ArrayView2<'_, f64>,
366    poly2: &ArrayView2<'_, f64>,
367) -> SpatialResult<Array2<f64>> {
368    // Symmetric difference = (A ∪ B) - (A ∩ B) = (A - B) ∪ (B - A)
369    let diff1 = polygon_difference(poly1, poly2)?;
370    let diff2 = polygon_difference(poly2, poly1)?;
371
372    // Union the two differences
373    polygon_union(&diff1.view(), &diff2.view())
374}
375
376/// Find intersections between edges of two polygons
377#[allow(dead_code)]
378fn find_intersections(poly1: &mut LabeledPolygon, poly2: &mut LabeledPolygon) -> SpatialResult<()> {
379    for (i, edge1) in poly1.edges.iter_mut().enumerate() {
380        for (j, edge2) in poly2.edges.iter_mut().enumerate() {
381            if let Some((intersectionpoint, t1, t2)) =
382                line_segment_intersection(&edge1.start, &edge1.end, &edge2.start, &edge2.end)
383            {
384                // Add intersection to both edges
385                edge1.intersectionpoints.push(IntersectionPoint {
386                    point: intersectionpoint,
387                    t: t1,
388                    other_edge_id: j,
389                });
390
391                edge2.intersectionpoints.push(IntersectionPoint {
392                    point: intersectionpoint,
393                    t: t2,
394                    other_edge_id: i,
395                });
396            }
397        }
398    }
399
400    // Sort intersection points along each edge
401    for edge in &mut poly1.edges {
402        edge.intersectionpoints
403            .sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap_or(Ordering::Equal));
404    }
405    for edge in &mut poly2.edges {
406        edge.intersectionpoints
407            .sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap_or(Ordering::Equal));
408    }
409
410    Ok(())
411}
412
413/// Find intersection of two line segments
414#[allow(dead_code)]
415fn line_segment_intersection(
416    p1: &Point2D,
417    p2: &Point2D,
418    p3: &Point2D,
419    p4: &Point2D,
420) -> Option<(Point2D, f64, f64)> {
421    let x1 = p1.x;
422    let y1 = p1.y;
423    let x2 = p2.x;
424    let y2 = p2.y;
425    let x3 = p3.x;
426    let y3 = p3.y;
427    let x4 = p4.x;
428    let y4 = p4.y;
429
430    let denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
431
432    if denom.abs() < 1e-10 {
433        // Lines are parallel
434        return None;
435    }
436
437    let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
438    let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
439
440    // Check if intersection is within both line segments
441    if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
442        let ix = x1 + t * (x2 - x1);
443        let iy = y1 + t * (y2 - y1);
444        let intersection = Point2D::new(ix, iy);
445        Some((intersection, t, u))
446    } else {
447        None
448    }
449}
450
451/// Sutherland-Hodgman polygon clipping algorithm for intersection
452#[allow(dead_code)]
453fn sutherland_hodgman_clip(
454    subject: &LabeledPolygon,
455    clip: &LabeledPolygon,
456) -> SpatialResult<LabeledPolygon> {
457    let mut outputvertices = subject.vertices.clone();
458
459    for i in 0..clip.vertices.len() {
460        if outputvertices.is_empty() {
461            break;
462        }
463
464        let clip_edge_start = clip.vertices[i];
465        let clip_edge_end = clip.vertices[(i + 1) % clip.vertices.len()];
466
467        let inputvertices = outputvertices.clone();
468        outputvertices.clear();
469
470        if !inputvertices.is_empty() {
471            let mut s = inputvertices[inputvertices.len() - 1];
472
473            for vertex in inputvertices {
474                if is_inside(&vertex, &clip_edge_start, &clip_edge_end) {
475                    if !is_inside(&s, &clip_edge_start, &clip_edge_end) {
476                        // Entering the clip region
477                        if let Some((intersection__, _, _)) =
478                            line_segment_intersection(&s, &vertex, &clip_edge_start, &clip_edge_end)
479                        {
480                            outputvertices.push(intersection__);
481                        }
482                    }
483                    outputvertices.push(vertex);
484                } else if is_inside(&s, &clip_edge_start, &clip_edge_end) {
485                    // Leaving the clip region
486                    if let Some((intersection__, _, _)) =
487                        line_segment_intersection(&s, &vertex, &clip_edge_start, &clip_edge_end)
488                    {
489                        outputvertices.push(intersection__);
490                    }
491                }
492                s = vertex;
493            }
494        }
495    }
496
497    // Build result polygon
498    if outputvertices.len() < 3 {
499        // Return empty polygon
500        outputvertices.clear();
501    }
502
503    let mut edges = Vec::new();
504    for i in 0..outputvertices.len() {
505        let start = outputvertices[i];
506        let end = outputvertices[(i + 1) % outputvertices.len()];
507        edges.push(Edge {
508            start,
509            end,
510            polygon_id: 0,
511            intersectionpoints: Vec::new(),
512        });
513    }
514
515    Ok(LabeledPolygon {
516        vertices: outputvertices,
517        edges,
518        is_hole: false,
519    })
520}
521
522/// Check if a point is inside relative to a directed edge
523#[allow(dead_code)]
524fn is_inside(point: &Point2D, edge_start: &Point2D, edgeend: &Point2D) -> bool {
525    let edge_vector = Point2D::new(edgeend.x - edge_start.x, edgeend.y - edge_start.y);
526    let point_vector = Point2D::new(point.x - edge_start.x, point.y - edge_start.y);
527    edge_vector.cross_product(&point_vector) >= 0.0
528}
529
530/// Weiler-Atherton algorithm for union operation
531#[allow(dead_code)]
532fn weiler_atherton_union(
533    poly1: &LabeledPolygon,
534    poly2: &LabeledPolygon,
535) -> SpatialResult<Vec<LabeledPolygon>> {
536    // Check if polygons don't intersect
537    if !polygons_intersect(poly1, poly2) {
538        // Return both polygons separately or the larger one
539        return Ok(vec![poly1.clone(), poly2.clone()]);
540    }
541
542    // Build the intersection graph
543    let intersection_graph = build_intersection_graph(poly1, poly2)?;
544
545    // Trace the union boundary
546    let result_polygons = trace_union_boundary(&intersection_graph, poly1, poly2)?;
547
548    Ok(result_polygons)
549}
550
551/// Weiler-Atherton algorithm for difference operation
552#[allow(dead_code)]
553fn weiler_atherton_difference(
554    poly1: &LabeledPolygon,
555    poly2: &LabeledPolygon,
556) -> SpatialResult<Vec<LabeledPolygon>> {
557    // Check if polygons don't intersect
558    if !polygons_intersect(poly1, poly2) {
559        // Return original polygon
560        return Ok(vec![poly1.clone()]);
561    }
562
563    // Build the intersection graph
564    let intersection_graph = build_intersection_graph(poly1, poly2)?;
565
566    // Trace the difference boundary
567    let result_polygons = trace_difference_boundary(&intersection_graph, poly1, poly2)?;
568
569    Ok(result_polygons)
570}
571
572/// Check if two polygons intersect
573#[allow(dead_code)]
574fn polygons_intersect(poly1: &LabeledPolygon, poly2: &LabeledPolygon) -> bool {
575    // Quick check: if any vertex of one polygon is inside the other
576    for vertex in &poly1.vertices {
577        if poly2.ispoint_inside(vertex) {
578            return true;
579        }
580    }
581
582    for vertex in &poly2.vertices {
583        if poly1.ispoint_inside(vertex) {
584            return true;
585        }
586    }
587
588    // Check for edge intersections
589    for edge1 in &poly1.edges {
590        for edge2 in &poly2.edges {
591            if line_segment_intersection(&edge1.start, &edge1.end, &edge2.start, &edge2.end)
592                .is_some()
593            {
594                return true;
595            }
596        }
597    }
598
599    false
600}
601
602/// Build intersection graph for Weiler-Atherton algorithm
603#[allow(dead_code)]
604fn build_intersection_graph(
605    poly1: &LabeledPolygon,
606    _poly2: &LabeledPolygon,
607) -> SpatialResult<HashMap<String, Vec<Point2D>>> {
608    // This is a simplified implementation
609    // A full implementation would build a proper intersection graph
610    // with entry/exit points and traversal information
611    Ok(HashMap::new())
612}
613
614/// Trace union boundary using intersection graph
615#[allow(dead_code)]
616fn trace_union_boundary(
617    _graph: &HashMap<String, Vec<Point2D>>,
618    poly1: &LabeledPolygon,
619    poly2: &LabeledPolygon,
620) -> SpatialResult<Vec<LabeledPolygon>> {
621    // Simplified implementation: return the larger polygon
622    // A complete implementation would properly trace the union boundary
623    if poly1.compute_area() >= poly2.compute_area() {
624        Ok(vec![poly1.clone()])
625    } else {
626        Ok(vec![poly2.clone()])
627    }
628}
629
630/// Trace difference boundary using intersection graph
631#[allow(dead_code)]
632fn trace_difference_boundary(
633    _graph: &HashMap<String, Vec<Point2D>>,
634    poly1: &LabeledPolygon,
635    _poly2: &LabeledPolygon,
636) -> SpatialResult<Vec<LabeledPolygon>> {
637    // Simplified implementation: return the first polygon
638    // A complete implementation would properly trace the difference boundary
639    Ok(vec![poly1.clone()])
640}
641
642/// Check if a polygon is convex
643#[allow(dead_code)]
644pub fn is_convex_polygon(vertices: &ArrayView2<'_, f64>) -> SpatialResult<bool> {
645    if vertices.ncols() != 2 {
646        return Err(SpatialError::ValueError("Vertices must be 2D".to_string()));
647    }
648
649    let n = vertices.nrows();
650    if n < 3 {
651        return Ok(false);
652    }
653
654    let mut sign = 0i32;
655
656    for i in 0..n {
657        let p1 = Point2D::new(vertices[[i, 0]], vertices[[i, 1]]);
658        let p2 = Point2D::new(vertices[[(i + 1) % n, 0]], vertices[[(i + 1) % n, 1]]);
659        let p3 = Point2D::new(vertices[[(i + 2) % n, 0]], vertices[[(i + 2) % n, 1]]);
660
661        let v1 = Point2D::new(p2.x - p1.x, p2.y - p1.y);
662        let v2 = Point2D::new(p3.x - p2.x, p3.y - p2.y);
663
664        let cross = v1.cross_product(&v2);
665
666        if cross.abs() > 1e-10 {
667            let current_sign = if cross > 0.0 { 1 } else { -1 };
668
669            if sign == 0 {
670                sign = current_sign;
671            } else if sign != current_sign {
672                return Ok(false);
673            }
674        }
675    }
676
677    Ok(true)
678}
679
680/// Compute the area of a polygon
681#[allow(dead_code)]
682pub fn compute_polygon_area(vertices: &ArrayView2<'_, f64>) -> SpatialResult<f64> {
683    let polygon = LabeledPolygon::from_array(vertices)?;
684    Ok(polygon.compute_area())
685}
686
687/// Check if a polygon is self-intersecting
688#[allow(dead_code)]
689pub fn is_self_intersecting(vertices: &ArrayView2<'_, f64>) -> SpatialResult<bool> {
690    let polygon = LabeledPolygon::from_array(vertices)?;
691    let n = polygon.vertices.len();
692
693    for i in 0..n {
694        let edge1_start = polygon.vertices[i];
695        let edge1_end = polygon.vertices[(i + 1) % n];
696
697        for j in (i + 2)..n {
698            // Skip adjacent edges
699            if j == (i + n - 1) % n {
700                continue;
701            }
702
703            let edge2_start = polygon.vertices[j];
704            let edge2_end = polygon.vertices[(j + 1) % n];
705
706            if line_segment_intersection(&edge1_start, &edge1_end, &edge2_start, &edge2_end)
707                .is_some()
708            {
709                return Ok(true);
710            }
711        }
712    }
713
714    Ok(false)
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720    use approx::assert_relative_eq;
721    use scirs2_core::ndarray::arr2;
722
723    #[test]
724    fn testpoint2d_operations() {
725        let p1 = Point2D::new(0.0, 0.0);
726        let p2 = Point2D::new(1.0, 1.0);
727
728        assert_relative_eq!(p1.distance_to(&p2), 2.0_f64.sqrt(), epsilon = 1e-10);
729
730        let v1 = Point2D::new(1.0, 0.0);
731        let v2 = Point2D::new(0.0, 1.0);
732        assert_relative_eq!(v1.cross_product(&v2), 1.0, epsilon = 1e-10);
733        assert_relative_eq!(v1.dot_product(&v2), 0.0, epsilon = 1e-10);
734    }
735
736    #[test]
737    fn test_polygon_creation() {
738        let vertices = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
739        let polygon = LabeledPolygon::from_array(&vertices.view()).unwrap();
740
741        assert_eq!(polygon.vertices.len(), 4);
742        assert_eq!(polygon.edges.len(), 4);
743        assert_relative_eq!(polygon.compute_area(), 1.0, epsilon = 1e-10);
744    }
745
746    #[test]
747    fn testpoint_inside_polygon() {
748        let vertices = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
749        let polygon = LabeledPolygon::from_array(&vertices.view()).unwrap();
750
751        let insidepoint = Point2D::new(0.5, 0.5);
752        let outsidepoint = Point2D::new(1.5, 1.5);
753
754        assert!(polygon.ispoint_inside(&insidepoint));
755        assert!(!polygon.ispoint_inside(&outsidepoint));
756    }
757
758    #[test]
759    fn test_line_intersection() {
760        let p1 = Point2D::new(0.0, 0.0);
761        let p2 = Point2D::new(1.0, 1.0);
762        let p3 = Point2D::new(0.0, 1.0);
763        let p4 = Point2D::new(1.0, 0.0);
764
765        let result = line_segment_intersection(&p1, &p2, &p3, &p4);
766        assert!(result.is_some());
767
768        let (intersection, t1, t2) = result.unwrap();
769        assert_relative_eq!(intersection.x, 0.5, epsilon = 1e-10);
770        assert_relative_eq!(intersection.y, 0.5, epsilon = 1e-10);
771        assert_relative_eq!(t1, 0.5, epsilon = 1e-10);
772        assert_relative_eq!(t2, 0.5, epsilon = 1e-10);
773    }
774
775    #[test]
776    fn test_is_convex_polygon() {
777        // Convex square
778        let convex = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
779        assert!(is_convex_polygon(&convex.view()).unwrap());
780
781        // Non-convex polygon (L-shape)
782        let non_convex = arr2(&[
783            [0.0, 0.0],
784            [1.0, 0.0],
785            [1.0, 0.5],
786            [0.5, 0.5],
787            [0.5, 1.0],
788            [0.0, 1.0],
789        ]);
790        assert!(!is_convex_polygon(&non_convex.view()).unwrap());
791    }
792
793    #[test]
794    fn test_polygon_area() {
795        let square = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
796        let area = compute_polygon_area(&square.view()).unwrap();
797        assert_relative_eq!(area, 1.0, epsilon = 1e-10);
798
799        let triangle = arr2(&[[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]]);
800        let area = compute_polygon_area(&triangle.view()).unwrap();
801        assert_relative_eq!(area, 0.5, epsilon = 1e-10);
802    }
803
804    #[test]
805    fn test_intersection() {
806        // Non-self-intersecting square
807        let square = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
808        assert!(!is_self_intersecting(&square.view()).unwrap());
809
810        // Self-intersecting bowtie
811        let bowtie = arr2(&[[0.0, 0.0], [1.0, 1.0], [1.0, 0.0], [0.0, 1.0]]);
812        assert!(is_self_intersecting(&bowtie.view()).unwrap());
813    }
814
815    #[test]
816    fn test_polygon_union_basic() {
817        // Two non-overlapping squares
818        let poly1 = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
819        let poly2 = arr2(&[[2.0, 0.0], [3.0, 0.0], [3.0, 1.0], [2.0, 1.0]]);
820
821        let union = polygon_union(&poly1.view(), &poly2.view()).unwrap();
822        assert!(union.nrows() >= 4); // Should have at least 4 vertices
823    }
824
825    #[test]
826    fn test_polygon_intersection_basic() {
827        // Two overlapping squares
828        let poly1 = arr2(&[[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]]);
829        let poly2 = arr2(&[[1.0, 1.0], [3.0, 1.0], [3.0, 3.0], [1.0, 3.0]]);
830
831        let intersection = polygon_intersection(&poly1.view(), &poly2.view()).unwrap();
832
833        // The intersection should be a unit square
834        if intersection.nrows() > 0 {
835            let area = compute_polygon_area(&intersection.view()).unwrap();
836            assert!(area > 0.0); // Should have non-zero area
837        }
838    }
839
840    #[test]
841    fn test_polygon_difference_basic() {
842        let poly1 = arr2(&[[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]]);
843        let poly2 = arr2(&[[1.0, 1.0], [3.0, 1.0], [3.0, 3.0], [1.0, 3.0]]);
844
845        let difference = polygon_difference(&poly1.view(), &poly2.view()).unwrap();
846        assert!(difference.nrows() >= 3); // Should have at least 3 vertices
847    }
848
849    #[test]
850    fn test_sutherland_hodgman_clip() {
851        let subjectvertices = vec![
852            Point2D::new(0.0, 0.0),
853            Point2D::new(2.0, 0.0),
854            Point2D::new(2.0, 2.0),
855            Point2D::new(0.0, 2.0),
856        ];
857
858        let clipvertices = vec![
859            Point2D::new(1.0, 1.0),
860            Point2D::new(3.0, 1.0),
861            Point2D::new(3.0, 3.0),
862            Point2D::new(1.0, 3.0),
863        ];
864
865        let subject = LabeledPolygon {
866            vertices: subjectvertices,
867            edges: Vec::new(),
868            is_hole: false,
869        };
870
871        let clip = LabeledPolygon {
872            vertices: clipvertices,
873            edges: Vec::new(),
874            is_hole: false,
875        };
876
877        let result = sutherland_hodgman_clip(&subject, &clip).unwrap();
878
879        // Should produce a valid polygon
880        assert!(result.vertices.len() >= 3);
881    }
882}