Skip to main content

bsp_tree/
cuttable.rs

1//! Polygon cutting/splitting operations for BSP trees.
2
3use crate::{Classification, Plane3D, PlaneSide, Polygon, Rectangle, Triangle};
4
5/// Trait for geometry that can be cut by a plane.
6pub trait Cuttable {
7    /// Cuts the geometry by a plane.
8    ///
9    /// Returns `(front, back)` where:
10    /// - `front`: `Some(polygon)` containing the part on the front side of the plane
11    /// - `back`: `Some(polygon)` containing the part on the back side of the plane
12    ///
13    /// # Return values by classification
14    ///
15    /// - **Front**: `(Some(self), None)` - entire geometry is in front
16    /// - **Back**: `(None, Some(self))` - entire geometry is behind
17    /// - **Coplanar**: `(Some(self), None)` - treated as front
18    /// - **Spanning**: `(Some(front_part), Some(back_part))` - split into two pieces
19    fn cut(&self, plane: &Plane3D) -> (Option<Polygon>, Option<Polygon>);
20}
21
22impl Cuttable for Polygon {
23    fn cut(&self, plane: &Plane3D) -> (Option<Polygon>, Option<Polygon>) {
24        match self.classify(plane) {
25            Classification::Front | Classification::Coplanar => {
26                (Some(self.clone()), None)
27            }
28            Classification::Back => {
29                (None, Some(self.clone()))
30            }
31            Classification::Spanning => {
32                split_polygon(self, plane)
33            }
34        }
35    }
36}
37
38/// Splits a spanning polygon into front and back parts.
39///
40/// Uses a variant of the Sutherland-Hodgman algorithm:
41/// walks the polygon edges and builds two vertex lists,
42/// adding intersection points when edges cross the plane.
43fn split_polygon(polygon: &Polygon, plane: &Plane3D) -> (Option<Polygon>, Option<Polygon>) {
44    let vertices = polygon.vertices();
45    let n = vertices.len();
46
47    let mut front_verts = Vec::with_capacity(n + 1);
48    let mut back_verts = Vec::with_capacity(n + 1);
49
50    // Classify all vertices upfront
51    let sides: Vec<PlaneSide> = vertices
52        .iter()
53        .map(|v| plane.classify_point(*v))
54        .collect();
55
56    for i in 0..n {
57        let current = vertices[i];
58        let current_side = sides[i];
59        let next_idx = (i + 1) % n;
60        let next = vertices[next_idx];
61        let next_side = sides[next_idx];
62
63        // Add current vertex to appropriate list(s)
64        match current_side {
65            PlaneSide::Front => front_verts.push(current),
66            PlaneSide::Back => back_verts.push(current),
67            PlaneSide::OnPlane => {
68                // On-plane vertices go to both sides
69                front_verts.push(current);
70                back_verts.push(current);
71            }
72        }
73
74        // Check if edge crosses the plane (excluding on-plane cases)
75        let crosses = matches!(
76            (current_side, next_side),
77            (PlaneSide::Front, PlaneSide::Back) | (PlaneSide::Back, PlaneSide::Front)
78        );
79
80        if crosses {
81            // Compute intersection point
82            if let Some((_, intersection)) = plane.intersect_segment(current, next) {
83                front_verts.push(intersection);
84                back_verts.push(intersection);
85            }
86        }
87    }
88
89    // Build result polygons (only if they have enough vertices)
90    let front = if front_verts.len() >= 3 {
91        Some(Polygon::new(front_verts))
92    } else {
93        None
94    };
95
96    let back = if back_verts.len() >= 3 {
97        Some(Polygon::new(back_verts))
98    } else {
99        None
100    };
101
102    (front, back)
103}
104
105impl Cuttable for Triangle {
106    fn cut(&self, plane: &Plane3D) -> (Option<Polygon>, Option<Polygon>) {
107        Polygon::from(self).cut(plane)
108    }
109}
110
111impl Cuttable for Rectangle {
112    fn cut(&self, plane: &Plane3D) -> (Option<Polygon>, Option<Polygon>) {
113        Polygon::from(self).cut(plane)
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use nalgebra::{Point3, Vector3};
121
122    // =========================================================================
123    // Helper functions
124    // =========================================================================
125
126    /// Creates a horizontal plane at the given height (normal pointing up).
127    fn horizontal_plane(height: f32) -> Plane3D {
128        Plane3D::from_point_and_normal(
129            Point3::new(0.0, height, 0.0),
130            Vector3::new(0.0, 1.0, 0.0),
131        )
132    }
133
134    /// Creates a vertical plane at x = offset (normal pointing in +X direction).
135    fn vertical_plane_x(offset: f32) -> Plane3D {
136        Plane3D::from_point_and_normal(
137            Point3::new(offset, 0.0, 0.0),
138            Vector3::new(1.0, 0.0, 0.0),
139        )
140    }
141
142    /// Creates a vertical plane at z = offset (normal pointing in +Z direction).
143    fn vertical_plane_z(offset: f32) -> Plane3D {
144        Plane3D::from_point_and_normal(
145            Point3::new(0.0, 0.0, offset),
146            Vector3::new(0.0, 0.0, 1.0),
147        )
148    }
149
150    /// Checks that all vertices of a polygon are on the expected side of the plane.
151    fn assert_all_vertices_on_side(polygon: &Polygon, plane: &Plane3D, expected: PlaneSide) {
152        for (i, v) in polygon.vertices().iter().enumerate() {
153            let side = plane.classify_point(*v);
154            assert!(
155                side == expected || side == PlaneSide::OnPlane,
156                "Vertex {i} at {v:?} is on {side:?}, expected {expected:?} or OnPlane"
157            );
158        }
159    }
160
161    /// Checks that a polygon has the expected number of vertices.
162    fn assert_vertex_count(polygon: &Polygon, expected: usize) {
163        assert_eq!(
164            polygon.len(),
165            expected,
166            "Expected {expected} vertices, got {}",
167            polygon.len()
168        );
169    }
170
171    /// Approximate equality for f32 with tolerance.
172    fn approx_eq(a: f32, b: f32, epsilon: f32) -> bool {
173        (a - b).abs() < epsilon
174    }
175
176    /// Checks that a point lies on the plane (within tolerance).
177    fn assert_point_on_plane(point: Point3<f32>, plane: &Plane3D) {
178        let dist = plane.signed_distance(point).abs();
179        assert!(
180            dist < 1e-4,
181            "Point {point:?} should be on plane, but distance is {dist}"
182        );
183    }
184
185    /// Checks if two vertex sequences represent the same polygon (rotation-independent).
186    ///
187    /// Returns true if `actual` contains the same vertices as `expected` in the same
188    /// cyclic order, but possibly starting at a different index.
189    fn same_polygon_vertices(actual: &[Point3<f32>], expected: &[Point3<f32>]) -> bool {
190        if actual.len() != expected.len() {
191            return false;
192        }
193        let n = actual.len();
194        if n == 0 {
195            return true;
196        }
197
198        // Find starting offset: where does expected[0] appear in actual?
199        let Some(offset) = actual.iter().position(|v| v == &expected[0]) else {
200            return false;
201        };
202
203        // Check if all vertices match with this rotation
204        for i in 0..n {
205            if actual[(offset + i) % n] != expected[i] {
206                return false;
207            }
208        }
209        true
210    }
211
212    // =========================================================================
213    // Polygon: Classification tests (non-spanning)
214    // =========================================================================
215
216    #[test]
217    fn polygon_entirely_in_front() {
218        // Triangle above the XZ plane at y=0
219        let polygon = Polygon::new(vec![
220            Point3::new(0.0, 1.0, 0.0),
221            Point3::new(1.0, 2.0, 0.0),
222            Point3::new(0.0, 1.5, 1.0),
223        ]);
224        let plane = horizontal_plane(0.0);
225
226        let (front, back) = polygon.cut(&plane);
227
228        assert!(front.is_some(), "Front should contain the polygon");
229        assert!(back.is_none(), "Back should be empty");
230        assert_eq!(front.unwrap().vertices(), polygon.vertices());
231    }
232
233    #[test]
234    fn polygon_entirely_behind() {
235        // Triangle below the XZ plane at y=0
236        let polygon = Polygon::new(vec![
237            Point3::new(0.0, -1.0, 0.0),
238            Point3::new(1.0, -2.0, 0.0),
239            Point3::new(0.0, -1.5, 1.0),
240        ]);
241        let plane = horizontal_plane(0.0);
242
243        let (front, back) = polygon.cut(&plane);
244
245        assert!(front.is_none(), "Front should be empty");
246        assert!(back.is_some(), "Back should contain the polygon");
247        assert_eq!(back.unwrap().vertices(), polygon.vertices());
248    }
249
250    #[test]
251    fn polygon_coplanar_with_plane() {
252        // Triangle lying exactly on the XZ plane
253        let polygon = Polygon::new(vec![
254            Point3::new(0.0, 0.0, 0.0),
255            Point3::new(1.0, 0.0, 0.0),
256            Point3::new(0.5, 0.0, 1.0),
257        ]);
258        let plane = horizontal_plane(0.0);
259
260        let (front, back) = polygon.cut(&plane);
261
262        assert!(front.is_some(), "Coplanar should be treated as front");
263        assert!(back.is_none(), "Back should be empty for coplanar");
264        assert_eq!(front.unwrap().vertices(), polygon.vertices());
265    }
266
267    // =========================================================================
268    // Polygon: Spanning/splitting tests
269    // =========================================================================
270
271    #[test]
272    fn polygon_split_triangle_one_vertex_front() {
273        // Triangle with one vertex above plane, two below
274        //     * (0, 2, 0) - front
275        //    / \
276        //   *---* (±1, -1, 0) - both back
277        let polygon = Polygon::new(vec![
278            Point3::new(0.0, 2.0, 0.0),  // front
279            Point3::new(-1.0, -1.0, 0.0), // back
280            Point3::new(1.0, -1.0, 0.0),  // back
281        ]);
282        let plane = horizontal_plane(0.0);
283
284        let (front, back) = polygon.cut(&plane);
285
286        assert!(front.is_some(), "Should have front part");
287        assert!(back.is_some(), "Should have back part");
288
289        let front = front.unwrap();
290        let back = back.unwrap();
291
292        // Front part should be a triangle (1 original + 2 intersection points)
293        assert_vertex_count(&front, 3);
294        // Back part should be a quad (2 original + 2 intersection points)
295        assert_vertex_count(&back, 4);
296
297        // Verify all front vertices are in front or on plane
298        assert_all_vertices_on_side(&front, &plane, PlaneSide::Front);
299        // Verify all back vertices are behind or on plane
300        assert_all_vertices_on_side(&back, &plane, PlaneSide::Back);
301    }
302
303    #[test]
304    fn polygon_split_triangle_two_vertices_front() {
305        // Triangle with two vertices above plane, one below
306        let polygon = Polygon::new(vec![
307            Point3::new(-1.0, 1.0, 0.0), // front
308            Point3::new(1.0, 1.0, 0.0),  // front
309            Point3::new(0.0, -2.0, 0.0), // back
310        ]);
311        let plane = horizontal_plane(0.0);
312
313        let (front, back) = polygon.cut(&plane);
314
315        assert!(front.is_some(), "Should have front part");
316        assert!(back.is_some(), "Should have back part");
317
318        let front = front.unwrap();
319        let back = back.unwrap();
320
321        // Front part should be a quad (2 original + 2 intersection points)
322        assert_vertex_count(&front, 4);
323        // Back part should be a triangle (1 original + 2 intersection points)
324        assert_vertex_count(&back, 3);
325
326        assert_all_vertices_on_side(&front, &plane, PlaneSide::Front);
327        assert_all_vertices_on_side(&back, &plane, PlaneSide::Back);
328    }
329
330    #[test]
331    fn polygon_split_quad_in_half() {
332        // Square split horizontally through the middle
333        // Two vertices above, two below
334        let polygon = Polygon::new(vec![
335            Point3::new(0.0, 1.0, 0.0),  // front
336            Point3::new(1.0, 1.0, 0.0),  // front
337            Point3::new(1.0, -1.0, 0.0), // back
338            Point3::new(0.0, -1.0, 0.0), // back
339        ]);
340        let plane = horizontal_plane(0.0);
341
342        let (front, back) = polygon.cut(&plane);
343
344        assert!(front.is_some());
345        assert!(back.is_some());
346
347        let front = front.unwrap();
348        let back = back.unwrap();
349
350        // Both halves should be quads
351        assert_vertex_count(&front, 4);
352        assert_vertex_count(&back, 4);
353
354        assert_all_vertices_on_side(&front, &plane, PlaneSide::Front);
355        assert_all_vertices_on_side(&back, &plane, PlaneSide::Back);
356    }
357
358    #[test]
359    fn polygon_split_pentagon() {
360        // Pentagon split by a plane
361        let polygon = Polygon::new(vec![
362            Point3::new(0.0, 2.0, 0.0),   // front
363            Point3::new(2.0, 1.0, 0.0),   // front
364            Point3::new(1.5, -1.0, 0.0),  // back
365            Point3::new(-1.5, -1.0, 0.0), // back
366            Point3::new(-2.0, 1.0, 0.0),  // front
367        ]);
368        let plane = horizontal_plane(0.0);
369
370        let (front, back) = polygon.cut(&plane);
371
372        assert!(front.is_some());
373        assert!(back.is_some());
374
375        let front = front.unwrap();
376        let back = back.unwrap();
377
378        // Front: 3 original vertices + 2 intersection points = 5
379        assert_vertex_count(&front, 5);
380        // Back: 2 original vertices + 2 intersection points = 4
381        assert_vertex_count(&back, 4);
382
383        assert_all_vertices_on_side(&front, &plane, PlaneSide::Front);
384        assert_all_vertices_on_side(&back, &plane, PlaneSide::Back);
385    }
386
387    #[test]
388    fn polygon_split_preserves_intersection_points_on_plane() {
389        // Verify that intersection points actually lie on the cutting plane
390        let polygon = Polygon::new(vec![
391            Point3::new(0.0, 3.0, 0.0),
392            Point3::new(2.0, -1.0, 1.0),
393            Point3::new(-2.0, -1.0, -1.0),
394        ]);
395        let plane = horizontal_plane(0.0);
396
397        let (front, back) = polygon.cut(&plane);
398
399        let front = front.unwrap();
400        let back = back.unwrap();
401
402        // Find intersection points (those that appear in both polygons)
403        for fv in front.vertices() {
404            if plane.classify_point(*fv) == PlaneSide::OnPlane {
405                assert_point_on_plane(*fv, &plane);
406            }
407        }
408        for bv in back.vertices() {
409            if plane.classify_point(*bv) == PlaneSide::OnPlane {
410                assert_point_on_plane(*bv, &plane);
411            }
412        }
413    }
414
415    // =========================================================================
416    // Polygon: Edge cases with vertices on the plane
417    // =========================================================================
418
419    #[test]
420    fn polygon_one_vertex_exactly_on_plane() {
421        // Triangle with one vertex on the plane, others in front
422        let polygon = Polygon::new(vec![
423            Point3::new(0.0, 0.0, 0.0), // on plane
424            Point3::new(1.0, 1.0, 0.0), // front
425            Point3::new(-1.0, 1.0, 0.0), // front
426        ]);
427        let plane = horizontal_plane(0.0);
428
429        let (front, back) = polygon.cut(&plane);
430
431        // Should classify as front (no vertices behind)
432        assert!(front.is_some());
433        assert!(back.is_none());
434    }
435
436    #[test]
437    fn polygon_one_vertex_on_plane_spanning() {
438        // Triangle with one vertex on plane, one front, one back
439        let polygon = Polygon::new(vec![
440            Point3::new(0.0, 0.0, 0.0),  // on plane
441            Point3::new(1.0, 1.0, 0.0),  // front
442            Point3::new(1.0, -1.0, 0.0), // back
443        ]);
444        let plane = horizontal_plane(0.0);
445
446        let (front, back) = polygon.cut(&plane);
447
448        assert!(front.is_some());
449        assert!(back.is_some());
450
451        let front = front.unwrap();
452        let back = back.unwrap();
453
454        // The on-plane vertex should appear in both polygons
455        let on_plane_point = Point3::new(0.0, 0.0, 0.0);
456        assert!(
457            front.vertices().contains(&on_plane_point),
458            "On-plane vertex should be in front polygon"
459        );
460        assert!(
461            back.vertices().contains(&on_plane_point),
462            "On-plane vertex should be in back polygon"
463        );
464    }
465
466    #[test]
467    fn polygon_edge_lies_on_plane() {
468        // Square where one edge lies exactly on the cutting plane
469        let polygon = Polygon::new(vec![
470            Point3::new(0.0, 0.0, 0.0), // on plane
471            Point3::new(1.0, 0.0, 0.0), // on plane
472            Point3::new(1.0, 1.0, 0.0), // front
473            Point3::new(0.0, 1.0, 0.0), // front
474        ]);
475        let plane = horizontal_plane(0.0);
476
477        let (front, back) = polygon.cut(&plane);
478
479        // All vertices are on plane or front, so should be front only
480        assert!(front.is_some());
481        assert!(back.is_none());
482    }
483
484    #[test]
485    fn polygon_edge_on_plane_with_back_vertices() {
486        // Square where one edge lies on plane, opposite edge is behind
487        let polygon = Polygon::new(vec![
488            Point3::new(0.0, 0.0, 0.0),  // on plane
489            Point3::new(1.0, 0.0, 0.0),  // on plane
490            Point3::new(1.0, -1.0, 0.0), // back
491            Point3::new(0.0, -1.0, 0.0), // back
492        ]);
493        let plane = horizontal_plane(0.0);
494
495        let (front, back) = polygon.cut(&plane);
496
497        // On-plane vertices go to both, but front has no "front" vertices
498        // This should classify as back since no vertices are strictly front
499        // Actually, let's check the classification logic...
500        // on_plane=2, front=0, back=2 => Classification::Back
501        assert!(front.is_none());
502        assert!(back.is_some());
503    }
504
505    #[test]
506    fn polygon_two_vertices_on_plane_spanning() {
507        // Hexagon with two non-adjacent vertices on the cutting plane
508        // This ensures the split polygons have non-collinear first 3 vertices
509        let polygon = Polygon::new(vec![
510            Point3::new(0.0, 1.0, 0.0),   // front
511            Point3::new(1.0, 0.0, 0.0),   // on plane
512            Point3::new(1.0, -1.0, 0.0),  // back
513            Point3::new(0.0, -1.0, 0.0),  // back
514            Point3::new(-1.0, 0.0, 0.0),  // on plane
515            Point3::new(-1.0, 1.0, 0.0),  // front
516        ]);
517        let plane = horizontal_plane(0.0);
518
519        let (front, back) = polygon.cut(&plane);
520
521        assert!(front.is_some());
522        assert!(back.is_some());
523
524        let front = front.unwrap();
525        let back = back.unwrap();
526
527        // On-plane vertices should appear in both
528        let on_plane_1 = Point3::new(1.0, 0.0, 0.0);
529        let on_plane_2 = Point3::new(-1.0, 0.0, 0.0);
530
531        assert!(
532            front.vertices().contains(&on_plane_1),
533            "On-plane vertex 1 should be in front polygon"
534        );
535        assert!(
536            front.vertices().contains(&on_plane_2),
537            "On-plane vertex 2 should be in front polygon"
538        );
539        assert!(
540            back.vertices().contains(&on_plane_1),
541            "On-plane vertex 1 should be in back polygon"
542        );
543        assert!(
544            back.vertices().contains(&on_plane_2),
545            "On-plane vertex 2 should be in back polygon"
546        );
547
548        // Verify exact vertex sequences (rotation-independent)
549        let expected_front = vec![
550            Point3::new(0.0, 1.0, 0.0),
551            Point3::new(1.0, 0.0, 0.0),
552            Point3::new(-1.0, 0.0, 0.0),
553            Point3::new(-1.0, 1.0, 0.0),
554        ];
555        let expected_back = vec![
556            Point3::new(1.0, 0.0, 0.0),
557            Point3::new(1.0, -1.0, 0.0),
558            Point3::new(0.0, -1.0, 0.0),
559            Point3::new(-1.0, 0.0, 0.0),
560        ];
561
562        assert!(
563            same_polygon_vertices(front.vertices(), &expected_front),
564            "Front polygon vertices mismatch.\nExpected: {expected_front:?}\nActual: {:?}",
565            front.vertices()
566        );
567        assert!(
568            same_polygon_vertices(back.vertices(), &expected_back),
569            "Back polygon vertices mismatch.\nExpected: {expected_back:?}\nActual: {:?}",
570            back.vertices()
571        );
572    }
573
574    // =========================================================================
575    // Triangle: Cuttable implementation tests
576    // =========================================================================
577
578    #[test]
579    fn triangle_entirely_front() {
580        let triangle = Triangle::new(
581            Point3::new(0.0, 1.0, 0.0),
582            Point3::new(1.0, 2.0, 0.0),
583            Point3::new(0.5, 1.5, 1.0),
584        );
585        let plane = horizontal_plane(0.0);
586
587        let (front, back) = triangle.cut(&plane);
588
589        assert!(front.is_some());
590        assert!(back.is_none());
591        assert_vertex_count(&front.unwrap(), 3);
592    }
593
594    #[test]
595    fn triangle_entirely_back() {
596        let triangle = Triangle::new(
597            Point3::new(0.0, -1.0, 0.0),
598            Point3::new(1.0, -2.0, 0.0),
599            Point3::new(0.5, -1.5, 1.0),
600        );
601        let plane = horizontal_plane(0.0);
602
603        let (front, back) = triangle.cut(&plane);
604
605        assert!(front.is_none());
606        assert!(back.is_some());
607        assert_vertex_count(&back.unwrap(), 3);
608    }
609
610    #[test]
611    fn triangle_coplanar() {
612        let triangle = Triangle::new(
613            Point3::new(0.0, 0.0, 0.0),
614            Point3::new(1.0, 0.0, 0.0),
615            Point3::new(0.5, 0.0, 1.0),
616        );
617        let plane = horizontal_plane(0.0);
618
619        let (front, back) = triangle.cut(&plane);
620
621        assert!(front.is_some());
622        assert!(back.is_none());
623    }
624
625    #[test]
626    fn triangle_split_one_front_two_back() {
627        let triangle = Triangle::new(
628            Point3::new(0.0, 2.0, 0.0),   // front
629            Point3::new(-1.0, -1.0, 0.0), // back
630            Point3::new(1.0, -1.0, 0.0),  // back
631        );
632        let plane = horizontal_plane(0.0);
633
634        let (front, back) = triangle.cut(&plane);
635
636        assert!(front.is_some());
637        assert!(back.is_some());
638
639        // One front vertex + 2 intersections = triangle
640        assert_vertex_count(&front.unwrap(), 3);
641        // Two back vertices + 2 intersections = quad
642        assert_vertex_count(&back.unwrap(), 4);
643    }
644
645    #[test]
646    fn triangle_split_two_front_one_back() {
647        let triangle = Triangle::new(
648            Point3::new(-1.0, 1.0, 0.0), // front
649            Point3::new(1.0, 1.0, 0.0),  // front
650            Point3::new(0.0, -2.0, 0.0), // back
651        );
652        let plane = horizontal_plane(0.0);
653
654        let (front, back) = triangle.cut(&plane);
655
656        assert!(front.is_some());
657        assert!(back.is_some());
658
659        // Two front vertices + 2 intersections = quad
660        assert_vertex_count(&front.unwrap(), 4);
661        // One back vertex + 2 intersections = triangle
662        assert_vertex_count(&back.unwrap(), 3);
663    }
664
665    // =========================================================================
666    // Rectangle: Cuttable implementation tests
667    // =========================================================================
668
669    #[test]
670    fn rectangle_entirely_front() {
671        let rect = Rectangle::new(
672            Point3::new(0.0, 1.0, 0.0),
673            Vector3::new(1.0, 0.0, 0.0),
674            Vector3::new(0.0, 1.0, 0.0),
675        );
676        let plane = horizontal_plane(0.0);
677
678        let (front, back) = rect.cut(&plane);
679
680        assert!(front.is_some());
681        assert!(back.is_none());
682        assert_vertex_count(&front.unwrap(), 4);
683    }
684
685    #[test]
686    fn rectangle_entirely_back() {
687        let rect = Rectangle::new(
688            Point3::new(0.0, -2.0, 0.0),
689            Vector3::new(1.0, 0.0, 0.0),
690            Vector3::new(0.0, 0.5, 0.0),
691        );
692        let plane = horizontal_plane(0.0);
693
694        let (front, back) = rect.cut(&plane);
695
696        assert!(front.is_none());
697        assert!(back.is_some());
698        assert_vertex_count(&back.unwrap(), 4);
699    }
700
701    #[test]
702    fn rectangle_coplanar() {
703        let rect = Rectangle::new(
704            Point3::new(0.0, 0.0, 0.0),
705            Vector3::new(1.0, 0.0, 0.0),
706            Vector3::new(0.0, 0.0, 1.0),
707        );
708        let plane = horizontal_plane(0.0);
709
710        let (front, back) = rect.cut(&plane);
711
712        assert!(front.is_some());
713        assert!(back.is_none());
714    }
715
716    #[test]
717    fn rectangle_split_horizontal() {
718        // Rectangle spanning y = -1 to y = 1, cut at y = 0
719        let rect = Rectangle::new(
720            Point3::new(0.0, -1.0, 0.0),
721            Vector3::new(2.0, 0.0, 0.0),
722            Vector3::new(0.0, 2.0, 0.0),
723        );
724        let plane = horizontal_plane(0.0);
725
726        let (front, back) = rect.cut(&plane);
727
728        assert!(front.is_some());
729        assert!(back.is_some());
730
731        // Both halves should be quads
732        assert_vertex_count(&front.unwrap(), 4);
733        assert_vertex_count(&back.unwrap(), 4);
734    }
735
736    #[test]
737    fn rectangle_split_diagonal() {
738        // Rectangle cut diagonally
739        let rect = Rectangle::new(
740            Point3::new(0.0, 0.0, 0.0),
741            Vector3::new(2.0, 0.0, 0.0),
742            Vector3::new(0.0, 0.0, 2.0),
743        );
744        // Diagonal plane through the rectangle
745        let plane = Plane3D::from_point_and_normal(
746            Point3::new(1.0, 0.0, 1.0),
747            Vector3::new(1.0, 0.0, 1.0),
748        );
749
750        let (front, back) = rect.cut(&plane);
751
752        assert!(front.is_some());
753        assert!(back.is_some());
754
755        // Diagonal cut through a rectangle creates two triangles
756        assert_vertex_count(&front.unwrap(), 3);
757        assert_vertex_count(&back.unwrap(), 3);
758    }
759
760    #[test]
761    fn rectangle_cut_off_corner() {
762        // Rectangle with one corner cut off
763        let rect = Rectangle::new(
764            Point3::new(0.0, 0.0, 0.0),
765            Vector3::new(2.0, 0.0, 0.0),
766            Vector3::new(0.0, 0.0, 2.0),
767        );
768        // Plane that cuts off just one corner
769        let plane = Plane3D::from_point_and_normal(
770            Point3::new(1.5, 0.0, 1.5),
771            Vector3::new(1.0, 0.0, 1.0),
772        );
773
774        let (front, back) = rect.cut(&plane);
775
776        assert!(front.is_some());
777        assert!(back.is_some());
778
779        let front = front.unwrap();
780        let back = back.unwrap();
781
782        // One side is a triangle (the cut corner)
783        // Other side is a pentagon (original 4 - 1 + 2 intersections)
784        let (small, large) = if front.len() < back.len() {
785            (front, back)
786        } else {
787            (back, front)
788        };
789
790        assert_vertex_count(&small, 3);
791        assert_vertex_count(&large, 5);
792    }
793
794    // =========================================================================
795    // Different plane orientations
796    // =========================================================================
797
798    #[test]
799    fn split_with_vertical_plane_x() {
800        // Triangle split by a vertical plane (x = 0)
801        let polygon = Polygon::new(vec![
802            Point3::new(-1.0, 0.0, 0.0), // back (negative x)
803            Point3::new(2.0, 0.0, 0.0),  // front (positive x)
804            Point3::new(0.5, 1.0, 0.0),  // front
805        ]);
806        let plane = vertical_plane_x(0.0);
807
808        let (front, back) = polygon.cut(&plane);
809
810        assert!(front.is_some());
811        assert!(back.is_some());
812
813        assert_all_vertices_on_side(&front.unwrap(), &plane, PlaneSide::Front);
814        assert_all_vertices_on_side(&back.unwrap(), &plane, PlaneSide::Back);
815    }
816
817    #[test]
818    fn split_with_vertical_plane_z() {
819        // Triangle split by a vertical plane (z = 0)
820        let polygon = Polygon::new(vec![
821            Point3::new(0.0, 0.0, -1.0), // back (negative z)
822            Point3::new(0.0, 0.0, 2.0),  // front (positive z)
823            Point3::new(0.0, 1.0, 0.5),  // front
824        ]);
825        let plane = vertical_plane_z(0.0);
826
827        let (front, back) = polygon.cut(&plane);
828
829        assert!(front.is_some());
830        assert!(back.is_some());
831
832        assert_all_vertices_on_side(&front.unwrap(), &plane, PlaneSide::Front);
833        assert_all_vertices_on_side(&back.unwrap(), &plane, PlaneSide::Back);
834    }
835
836    #[test]
837    fn split_with_tilted_plane() {
838        // Triangle split by a 45-degree tilted plane
839        let polygon = Polygon::new(vec![
840            Point3::new(0.0, 0.0, 0.0),
841            Point3::new(2.0, 2.0, 0.0),
842            Point3::new(2.0, 0.0, 0.0),
843        ]);
844        // Plane at 45 degrees: x + y = 1
845        let plane = Plane3D::from_point_and_normal(
846            Point3::new(0.5, 0.5, 0.0),
847            Vector3::new(1.0, 1.0, 0.0),
848        );
849
850        let (front, back) = polygon.cut(&plane);
851
852        assert!(front.is_some());
853        assert!(back.is_some());
854
855        assert_all_vertices_on_side(&front.unwrap(), &plane, PlaneSide::Front);
856        assert_all_vertices_on_side(&back.unwrap(), &plane, PlaneSide::Back);
857    }
858
859    // =========================================================================
860    // Geometric correctness tests
861    // =========================================================================
862
863    #[test]
864    fn split_intersection_points_are_correct() {
865        // Simple case: triangle from (-1, -1, 0) to (1, 1, 0) to (-1, 1, 0)
866        // Split at y = 0
867        let polygon = Polygon::new(vec![
868            Point3::new(0.0, -1.0, 0.0),
869            Point3::new(1.0, 1.0, 0.0),
870            Point3::new(-1.0, 1.0, 0.0),
871        ]);
872        let plane = horizontal_plane(0.0);
873
874        let (front, back) = polygon.cut(&plane);
875
876        let front = front.unwrap();
877        let _back = back.unwrap();
878
879        // Check that we can find the expected intersection points
880        // Edge from (0, -1, 0) to (1, 1, 0) intersects at (0.5, 0, 0)
881        // Edge from (0, -1, 0) to (-1, 1, 0) intersects at (-0.5, 0, 0)
882
883        let has_intersection_1 = front.vertices().iter().any(|v| {
884            approx_eq(v.x, 0.5, 1e-5) && approx_eq(v.y, 0.0, 1e-5)
885        });
886        let has_intersection_2 = front.vertices().iter().any(|v| {
887            approx_eq(v.x, -0.5, 1e-5) && approx_eq(v.y, 0.0, 1e-5)
888        });
889
890        assert!(has_intersection_1, "Should have intersection at (0.5, 0, 0)");
891        assert!(has_intersection_2, "Should have intersection at (-0.5, 0, 0)");
892    }
893
894    #[test]
895    fn split_preserves_z_coordinates() {
896        // Verify that splitting preserves Z coordinates correctly
897        let polygon = Polygon::new(vec![
898            Point3::new(0.0, 1.0, 5.0),
899            Point3::new(1.0, 1.0, 5.0),
900            Point3::new(1.0, -1.0, 5.0),
901            Point3::new(0.0, -1.0, 5.0),
902        ]);
903        let plane = horizontal_plane(0.0);
904
905        let (front, back) = polygon.cut(&plane);
906
907        // All vertices should have z = 5
908        for v in front.unwrap().vertices() {
909            assert!(
910                approx_eq(v.z, 5.0, 1e-5),
911                "Z coordinate should be preserved: got {}",
912                v.z
913            );
914        }
915        for v in back.unwrap().vertices() {
916            assert!(
917                approx_eq(v.z, 5.0, 1e-5),
918                "Z coordinate should be preserved: got {}",
919                v.z
920            );
921        }
922    }
923
924    #[test]
925    fn split_produces_valid_polygons() {
926        // Verify that split polygons pass validity checks
927        let polygon = Polygon::new(vec![
928            Point3::new(0.0, 2.0, 0.0),
929            Point3::new(2.0, 0.0, 1.0),
930            Point3::new(0.0, -2.0, 0.0),
931            Point3::new(-2.0, 0.0, -1.0),
932        ]);
933        let plane = horizontal_plane(0.0);
934
935        let (front, back) = polygon.cut(&plane);
936
937        let front = front.unwrap();
938        let back = back.unwrap();
939
940        // Both polygons should have at least 3 vertices
941        assert!(front.len() >= 3);
942        assert!(back.len() >= 3);
943
944        // Both polygons should be able to compute a normal (not degenerate)
945        assert!(front.unit_normal().is_some());
946        assert!(back.unit_normal().is_some());
947    }
948
949    // =========================================================================
950    // Edge cases and boundary conditions
951    // =========================================================================
952
953    #[test]
954    fn very_thin_slice() {
955        // Polygon where the cut produces a very thin slice
956        let polygon = Polygon::new(vec![
957            Point3::new(0.0, 0.001, 0.0),  // barely front
958            Point3::new(1.0, 0.001, 0.0),  // barely front
959            Point3::new(1.0, -1.0, 0.0),   // back
960            Point3::new(0.0, -1.0, 0.0),   // back
961        ]);
962        let plane = horizontal_plane(0.0);
963
964        let (front, back) = polygon.cut(&plane);
965
966        // Should still produce valid results
967        assert!(front.is_some());
968        assert!(back.is_some());
969    }
970
971    #[test]
972    fn large_polygon_many_vertices() {
973        // Hexagon split in half
974        let polygon = Polygon::new(vec![
975            Point3::new(1.0, 0.0, 0.0),
976            Point3::new(0.5, 0.866, 0.0),
977            Point3::new(-0.5, 0.866, 0.0),
978            Point3::new(-1.0, 0.0, 0.0),
979            Point3::new(-0.5, -0.866, 0.0),
980            Point3::new(0.5, -0.866, 0.0),
981        ]);
982        let plane = horizontal_plane(0.0);
983
984        let (front, back) = polygon.cut(&plane);
985
986        assert!(front.is_some());
987        assert!(back.is_some());
988
989        // Each half should be a quad (hexagon split horizontally)
990        assert_vertex_count(&front.unwrap(), 4);
991        assert_vertex_count(&back.unwrap(), 4);
992    }
993
994    #[test]
995    fn polygon_with_offset_plane() {
996        // Test with a plane not passing through origin
997        let polygon = Polygon::new(vec![
998            Point3::new(0.0, 5.0, 0.0),
999            Point3::new(1.0, 5.0, 0.0),
1000            Point3::new(1.0, 3.0, 0.0),
1001            Point3::new(0.0, 3.0, 0.0),
1002        ]);
1003        let plane = horizontal_plane(4.0); // y = 4
1004
1005        let (front, back) = polygon.cut(&plane);
1006
1007        assert!(front.is_some());
1008        assert!(back.is_some());
1009
1010        assert_all_vertices_on_side(&front.unwrap(), &plane, PlaneSide::Front);
1011        assert_all_vertices_on_side(&back.unwrap(), &plane, PlaneSide::Back);
1012    }
1013
1014    #[test]
1015    fn flipped_plane_reverses_classification() {
1016        let polygon = Polygon::new(vec![
1017            Point3::new(0.0, 1.0, 0.0),
1018            Point3::new(1.0, 1.0, 0.0),
1019            Point3::new(0.5, 2.0, 0.0),
1020        ]);
1021
1022        let plane = horizontal_plane(0.0);
1023        let flipped_plane = plane.flipped();
1024
1025        let (front1, back1) = polygon.cut(&plane);
1026        let (front2, back2) = polygon.cut(&flipped_plane);
1027
1028        // Original: polygon is in front
1029        assert!(front1.is_some());
1030        assert!(back1.is_none());
1031
1032        // Flipped: polygon is in back
1033        assert!(front2.is_none());
1034        assert!(back2.is_some());
1035    }
1036
1037    // =========================================================================
1038    // Regression tests
1039    // =========================================================================
1040
1041    #[test]
1042    fn split_does_not_produce_degenerate_polygons() {
1043        // Various configurations that could potentially produce degenerate results
1044        let test_cases = vec![
1045            // Triangle with apex at plane
1046            vec![
1047                Point3::new(0.0, 0.0, 0.0),
1048                Point3::new(1.0, 1.0, 0.0),
1049                Point3::new(-1.0, 1.0, 0.0),
1050            ],
1051            // Quad with edge on plane
1052            vec![
1053                Point3::new(-1.0, 0.0, 0.0),
1054                Point3::new(1.0, 0.0, 0.0),
1055                Point3::new(1.0, 1.0, 0.0),
1056                Point3::new(-1.0, 1.0, 0.0),
1057            ],
1058        ];
1059
1060        let plane = horizontal_plane(0.0);
1061
1062        for vertices in test_cases {
1063            let polygon = Polygon::new(vertices);
1064            let (front, back) = polygon.cut(&plane);
1065
1066            if let Some(f) = front {
1067                assert!(f.len() >= 3, "Front polygon is degenerate");
1068            }
1069            if let Some(b) = back {
1070                assert!(b.len() >= 3, "Back polygon is degenerate");
1071            }
1072        }
1073    }
1074
1075    #[test]
1076    fn consistent_results_with_equivalent_planes() {
1077        let polygon = Polygon::new(vec![
1078            Point3::new(0.0, 1.0, 0.0),
1079            Point3::new(1.0, -1.0, 0.0),
1080            Point3::new(-1.0, -1.0, 0.0),
1081        ]);
1082
1083        // Same plane, created two different ways
1084        let plane1 = horizontal_plane(0.0);
1085        let plane2 = Plane3D::new(Vector3::new(0.0, 1.0, 0.0), 0.0);
1086
1087        let (front1, back1) = polygon.cut(&plane1);
1088        let (front2, back2) = polygon.cut(&plane2);
1089
1090        // Should produce identical results
1091        assert_eq!(front1.as_ref().map(|p| p.len()), front2.as_ref().map(|p| p.len()));
1092        assert_eq!(back1.as_ref().map(|p| p.len()), back2.as_ref().map(|p| p.len()));
1093    }
1094}