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}