bsp_tree/rectangle.rs
1//! Rectangle (quad) representation for BSP trees.
2
3use nalgebra::{Point3, Vector3};
4
5use crate::{Classification, Plane3D, PlaneSide};
6
7/// A rectangle (quad) in 3D space, defined by a corner and two edge vectors.
8///
9/// The four vertices are:
10/// - `origin`
11/// - `origin + u`
12/// - `origin + u + v`
13/// - `origin + v`
14#[derive(Debug, Clone, PartialEq)]
15pub struct Rectangle {
16 origin: Point3<f32>,
17 u: Vector3<f32>,
18 v: Vector3<f32>,
19}
20
21impl Rectangle {
22 /// Creates a new rectangle from an origin corner and two edge vectors.
23 ///
24 /// The vertices will be: origin, origin+u, origin+u+v, origin+v (counter-clockwise).
25 pub fn new(origin: Point3<f32>, u: Vector3<f32>, v: Vector3<f32>) -> Self {
26 Self { origin, u, v }
27 }
28
29 /// Creates a rectangle from four corner points.
30 ///
31 /// The winding order should be: a -> b -> c -> d (counter-clockwise).
32 ///
33 /// Internally computes u = b - a and v = d - a.
34 ///
35 /// # Panics (debug builds only)
36 /// Panics if the points are not coplanar.
37 pub fn from_corners(
38 a: Point3<f32>,
39 b: Point3<f32>,
40 c: Point3<f32>,
41 d: Point3<f32>,
42 ) -> Self {
43 debug_assert!(
44 {
45 let plane = Plane3D::from_three_points(a, b, d);
46 plane.classify_point(c) == PlaneSide::OnPlane
47 },
48 "Rectangle corners must be coplanar"
49 );
50 let u = b - a;
51 let v = d - a;
52 Self { origin: a, u, v }
53 }
54
55 /// Returns the origin corner of the rectangle.
56 #[inline]
57 pub fn origin(&self) -> Point3<f32> {
58 self.origin
59 }
60
61 /// Returns the first edge vector.
62 #[inline]
63 pub fn u(&self) -> Vector3<f32> {
64 self.u
65 }
66
67 /// Returns the second edge vector.
68 #[inline]
69 pub fn v(&self) -> Vector3<f32> {
70 self.v
71 }
72
73 /// Returns the four vertices of the rectangle.
74 ///
75 /// Order: origin, origin+u, origin+u+v, origin+v (counter-clockwise).
76 pub fn vertices(&self) -> [Point3<f32>; 4] {
77 [
78 self.origin,
79 self.origin + self.u,
80 self.origin + self.u + self.v,
81 self.origin + self.v,
82 ]
83 }
84
85 /// Computes the (unnormalized) normal vector of the rectangle.
86 ///
87 /// The direction follows the right-hand rule: u × v.
88 pub fn normal(&self) -> Vector3<f32> {
89 self.u.cross(&self.v)
90 }
91
92 /// Computes the unit normal vector of the rectangle.
93 ///
94 /// Returns `None` if the rectangle is degenerate (zero area).
95 pub fn unit_normal(&self) -> Option<Vector3<f32>> {
96 let n = self.normal();
97 let len = n.norm();
98 if len > f32::EPSILON {
99 Some(n / len)
100 } else {
101 None
102 }
103 }
104
105 /// Returns the plane that this rectangle lies on.
106 ///
107 /// # Panics
108 /// Panics if the rectangle is degenerate (u and v are parallel).
109 pub fn plane(&self) -> Plane3D {
110 Plane3D::from_point_and_normal(self.origin, self.normal())
111 }
112
113 /// Computes the centroid (center) of the rectangle.
114 pub fn centroid(&self) -> Point3<f32> {
115 self.origin + (self.u + self.v) * 0.5
116 }
117
118 /// Computes the area of the rectangle.
119 pub fn area(&self) -> f32 {
120 self.normal().norm()
121 }
122
123 /// Classifies this rectangle relative to a plane.
124 ///
125 /// Returns:
126 /// - `Front` if all vertices are in front of the plane
127 /// - `Back` if all vertices are behind the plane
128 /// - `Coplanar` if all vertices lie on the plane
129 /// - `Spanning` if vertices are on both sides
130 pub fn classify(&self, plane: &Plane3D) -> Classification {
131 let mut front = 0;
132 let mut back = 0;
133 let mut on_plane = 0;
134
135 for vertex in self.vertices() {
136 match plane.classify_point(vertex) {
137 PlaneSide::Front => front += 1,
138 PlaneSide::Back => back += 1,
139 PlaneSide::OnPlane => on_plane += 1,
140 }
141 }
142
143 if on_plane == 4 {
144 Classification::Coplanar
145 } else if back == 0 {
146 Classification::Front
147 } else if front == 0 {
148 Classification::Back
149 } else {
150 Classification::Spanning
151 }
152 }
153}
154
155impl From<Rectangle> for Plane3D {
156 fn from(rectangle: Rectangle) -> Self {
157 rectangle.plane()
158 }
159}
160
161impl From<&Rectangle> for Plane3D {
162 fn from(rectangle: &Rectangle) -> Self {
163 rectangle.plane()
164 }
165}