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}