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