parry3d/shape/half_space.rs
1//! Support mapping based HalfSpace shape.
2use crate::math::{Real, Vector};
3use na::Unit;
4
5#[cfg(feature = "rkyv")]
6use rkyv::{bytecheck, CheckBytes};
7
8/// A half-space delimited by an infinite plane.
9///
10/// # What is a HalfSpace?
11///
12/// A half-space represents an infinite region of space on one side of a plane. It divides
13/// space into two regions:
14/// - The "inside" region (where the normal vector points away from)
15/// - The "outside" region (where the normal vector points toward)
16///
17/// The plane itself passes through the origin of the shape's coordinate system and is defined
18/// by its outward normal vector. All points in the direction opposite to the normal are
19/// considered "inside" the half-space.
20///
21/// # When to Use HalfSpace
22///
23/// Half-spaces are useful for representing:
24/// - **Ground planes**: A flat, infinite floor for collision detection
25/// - **Walls**: Infinite vertical barriers
26/// - **Bounding regions**: Constraining objects to one side of a plane
27/// - **Clipping planes**: Cutting off geometry in one direction
28///
29/// Because half-spaces are infinite, they are very efficient for collision detection and
30/// don't require complex shape representations.
31///
32/// # Coordinate System
33///
34/// The plane always passes through the origin `(0, 0)` in 2D or `(0, 0, 0)` in 3D of the
35/// half-space's local coordinate system. To position the plane elsewhere in your world,
36/// use an [`Isometry`](na::Isometry) transformation when performing queries.
37///
38/// # Examples
39///
40/// ## Creating a Ground Plane (3D)
41///
42/// ```
43/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
44/// use parry3d::shape::HalfSpace;
45/// use parry3d::na::{Vector3, Unit};
46///
47/// // Create a horizontal ground plane with normal pointing up (positive Y-axis)
48/// let ground = HalfSpace::new(Unit::new_normalize(Vector3::y()));
49///
50/// // The ground plane is at Y = 0 in local coordinates
51/// // Everything below (negative Y) is "inside" the half-space
52/// # }
53/// ```
54///
55/// ## Vertical Wall (2D)
56///
57/// ```
58/// # #[cfg(all(feature = "dim2", feature = "f32"))] {
59/// use parry2d::shape::HalfSpace;
60/// use parry2d::na::{Vector2, Unit};
61///
62/// // Create a vertical wall with normal pointing right (positive X-axis)
63/// let wall = HalfSpace::new(Unit::new_normalize(Vector2::x()));
64///
65/// // The wall is at X = 0 in local coordinates
66/// // Everything to the left (negative X) is "inside" the half-space
67/// # }
68/// ```
69///
70/// ## Collision Detection with a Ball (3D)
71///
72/// ```
73/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
74/// use parry3d::shape::{HalfSpace, Ball};
75/// use parry3d::query;
76/// use parry3d::na::{Isometry3, Vector3, Unit};
77///
78/// // Create a ground plane at Y = 0, normal pointing up
79/// let ground = HalfSpace::new(Unit::new_normalize(Vector3::y()));
80/// let ground_pos = Isometry3::identity();
81///
82/// // Create a ball with radius 1.0 at position (0, 0.5, 0)
83/// // The ball is resting on the ground, just touching it
84/// let ball = Ball::new(1.0);
85/// let ball_pos = Isometry3::translation(0.0, 0.5, 0.0);
86///
87/// // Check if they're in contact (with a small prediction distance)
88/// let contact = query::contact(
89/// &ground_pos,
90/// &ground,
91/// &ball_pos,
92/// &ball,
93/// 0.1
94/// );
95///
96/// assert!(contact.unwrap().is_some());
97/// # }
98/// ```
99///
100/// ## Positioned Ground Plane (3D)
101///
102/// ```
103/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
104/// use parry3d::shape::HalfSpace;
105/// use parry3d::query::{PointQuery};
106/// use parry3d::na::{Isometry3, Vector3, Point3, Unit};
107///
108/// // Create a ground plane with normal pointing up
109/// let ground = HalfSpace::new(Unit::new_normalize(Vector3::y()));
110///
111/// // Position the plane at Y = 5.0 using an isometry
112/// let ground_pos = Isometry3::translation(0.0, 5.0, 0.0);
113///
114/// // Check if a point is below the ground (inside the half-space)
115/// let point = Point3::new(0.0, 3.0, 0.0); // Point at Y = 3.0 (below the plane)
116///
117/// // Project the point onto the ground plane
118/// let proj = ground.project_point(&ground_pos, &point, true);
119///
120/// // The point is below the ground (inside the half-space)
121/// assert!(proj.is_inside);
122/// # }
123/// ```
124///
125/// ## Tilted Plane (3D)
126///
127/// ```
128/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
129/// use parry3d::shape::HalfSpace;
130/// use parry3d::na::{Vector3, Unit};
131///
132/// // Create a plane tilted at 45 degrees
133/// // Normal points up and to the right
134/// let normal = Vector3::new(1.0, 1.0, 0.0);
135/// let tilted_plane = HalfSpace::new(Unit::new_normalize(normal));
136///
137/// // This plane passes through the origin and divides space diagonally
138/// # }
139/// ```
140#[derive(PartialEq, Debug, Clone, Copy)]
141#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
142#[cfg_attr(
143 feature = "rkyv",
144 derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, CheckBytes),
145 archive(as = "Self")
146)]
147#[repr(C)]
148pub struct HalfSpace {
149 /// The halfspace planar boundary's outward normal.
150 ///
151 /// This unit vector points in the direction considered "outside" the half-space.
152 /// All points in the direction opposite to this normal (when measured from the
153 /// plane at the origin) are considered "inside" the half-space.
154 ///
155 /// # Example
156 ///
157 /// ```
158 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
159 /// use parry3d::shape::HalfSpace;
160 /// use parry3d::na::{Vector3, Unit};
161 ///
162 /// let ground = HalfSpace::new(Unit::new_normalize(Vector3::y()));
163 ///
164 /// // The normal points up (positive Y direction)
165 /// assert_eq!(*ground.normal, Vector3::y());
166 /// # }
167 /// ```
168 pub normal: Unit<Vector<Real>>,
169}
170
171impl HalfSpace {
172 /// Builds a new half-space from its outward normal vector.
173 ///
174 /// The plane defining the half-space passes through the origin of the local coordinate
175 /// system and is perpendicular to the given normal vector. The normal points toward
176 /// the "outside" region, while the opposite direction is considered "inside."
177 ///
178 /// # Parameters
179 ///
180 /// * `normal` - A unit vector defining the plane's outward normal direction. This must
181 /// be a normalized vector (use [`Unit::new_normalize`](na::Unit::new_normalize) to
182 /// create one from any vector).
183 ///
184 /// # Examples
185 ///
186 /// ## Creating a Horizontal Ground Plane (3D)
187 ///
188 /// ```
189 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
190 /// use parry3d::shape::HalfSpace;
191 /// use parry3d::na::{Vector3, Unit};
192 ///
193 /// // Ground plane with normal pointing up
194 /// let ground = HalfSpace::new(Unit::new_normalize(Vector3::y()));
195 /// # }
196 /// ```
197 ///
198 /// ## Creating a Vertical Wall (2D)
199 ///
200 /// ```
201 /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
202 /// use parry2d::shape::HalfSpace;
203 /// use parry2d::na::{Vector2, Unit};
204 ///
205 /// // Wall with normal pointing to the right
206 /// let wall = HalfSpace::new(Unit::new_normalize(Vector2::x()));
207 /// # }
208 /// ```
209 ///
210 /// ## Custom Normal Direction (3D)
211 ///
212 /// ```
213 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
214 /// use parry3d::shape::HalfSpace;
215 /// use parry3d::na::{Vector3, Unit};
216 ///
217 /// // Plane with normal at 45-degree angle
218 /// let custom_normal = Vector3::new(1.0, 1.0, 0.0);
219 /// let plane = HalfSpace::new(Unit::new_normalize(custom_normal));
220 ///
221 /// // Verify the normal is normalized
222 /// assert!((plane.normal.norm() - 1.0).abs() < 1e-5);
223 /// # }
224 /// ```
225 #[inline]
226 pub fn new(normal: Unit<Vector<Real>>) -> HalfSpace {
227 HalfSpace { normal }
228 }
229
230 /// Computes a scaled version of this half-space.
231 ///
232 /// Scaling a half-space applies non-uniform scaling to its normal vector. This is useful
233 /// when transforming shapes in a scaled coordinate system. The resulting normal is
234 /// re-normalized to maintain the half-space's validity.
235 ///
236 /// # Parameters
237 ///
238 /// * `scale` - A vector containing the scaling factors for each axis. For example,
239 /// `Vector3::new(2.0, 1.0, 1.0)` doubles the X-axis scaling.
240 ///
241 /// # Returns
242 ///
243 /// * `Some(HalfSpace)` - The scaled half-space with the transformed normal
244 /// * `None` - If the scaled normal becomes zero (degenerate case), meaning the
245 /// half-space cannot be represented after scaling
246 ///
247 /// # When This Returns None
248 ///
249 /// The method returns `None` when any component of the normal becomes zero after
250 /// scaling AND that component was the only non-zero component. For example:
251 /// - A horizontal plane (normal = `[0, 1, 0]`) scaled by `[1, 0, 1]` → `None`
252 /// - A diagonal plane (normal = `[0.7, 0.7, 0]`) scaled by `[1, 0, 1]` → `Some(...)`
253 ///
254 /// # Examples
255 ///
256 /// ## Uniform Scaling (3D)
257 ///
258 /// ```
259 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
260 /// use parry3d::shape::HalfSpace;
261 /// use parry3d::na::{Vector3, Unit};
262 ///
263 /// let ground = HalfSpace::new(Unit::new_normalize(Vector3::y()));
264 ///
265 /// // Uniform scaling doesn't change the normal direction
266 /// let scaled = ground.scaled(&Vector3::new(2.0, 2.0, 2.0)).unwrap();
267 /// assert_eq!(*scaled.normal, Vector3::y());
268 /// # }
269 /// ```
270 ///
271 /// ## Non-Uniform Scaling (3D)
272 ///
273 /// ```
274 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
275 /// use parry3d::shape::HalfSpace;
276 /// use parry3d::na::{Vector3, Unit};
277 ///
278 /// // Diagonal plane
279 /// let plane = HalfSpace::new(
280 /// Unit::new_normalize(Vector3::new(1.0, 1.0, 0.0))
281 /// );
282 ///
283 /// // Scale X-axis by 2.0, Y-axis stays 1.0
284 /// let scaled = plane.scaled(&Vector3::new(2.0, 1.0, 1.0)).unwrap();
285 ///
286 /// // The normal changes direction due to non-uniform scaling
287 /// // It's no longer at 45 degrees
288 /// assert!(scaled.normal.x != scaled.normal.y);
289 /// # }
290 /// ```
291 ///
292 /// ## Degenerate Case (3D)
293 ///
294 /// ```
295 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
296 /// use parry3d::shape::HalfSpace;
297 /// use parry3d::na::{Vector3, Unit};
298 ///
299 /// // Horizontal ground plane
300 /// let ground = HalfSpace::new(Unit::new_normalize(Vector3::y()));
301 ///
302 /// // Scaling Y to zero makes the normal degenerate
303 /// let scaled = ground.scaled(&Vector3::new(1.0, 0.0, 1.0));
304 /// assert!(scaled.is_none()); // Returns None because normal becomes zero
305 /// # }
306 /// ```
307 ///
308 /// ## Practical Use Case (2D)
309 ///
310 /// ```
311 /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
312 /// use parry2d::shape::HalfSpace;
313 /// use parry2d::na::{Vector2, Unit};
314 ///
315 /// // Create a wall in a 2D platformer
316 /// let wall = HalfSpace::new(Unit::new_normalize(Vector2::x()));
317 ///
318 /// // Apply level scaling (e.g., for pixel-perfect rendering)
319 /// let pixel_scale = Vector2::new(16.0, 16.0);
320 /// if let Some(scaled_wall) = wall.scaled(&pixel_scale) {
321 /// // Use the scaled wall for collision detection
322 /// }
323 /// # }
324 /// ```
325 pub fn scaled(self, scale: &Vector<Real>) -> Option<Self> {
326 Unit::try_new(self.normal.component_mul(scale), 0.0).map(|normal| Self { normal })
327 }
328}