impact_rs/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/piot/impact-rs
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5
6/*!
7This crate provides utilities for performing collision queries between rectangles and rays,
8including swept checks for moving rectangles. It leverages fixed-point arithmetic provided by the [`fixed32`] crate to
9handle the computations.
10 */
11
12use std::cmp::{max, min, Ordering};
13
14use fixed32::Fp;
15use fixed32_math::{Rect, Vector};
16
17pub mod prelude;
18
19#[derive(Debug, Clone)]
20pub struct RayIntersectionResult {
21    pub contact_point: Vector,
22    pub contact_normal: Vector,
23    pub closest_time: Fp,
24}
25
26/// Checks for intersection between a swept rectangle and a target rectangle.
27///
28/// This function determines if a rectangle, which is moving along a vector
29/// from its initial position, intersects with a target rectangle at any point
30/// during its motion. The swept rectangle is calculated based on the initial
31/// lower-left position and size of the origin rectangle, expanded to account for its
32/// movement. The function performs a ray-rectangle intersection test to
33/// check if there is an intersection within the valid time range.
34///
35/// # Parameters
36///
37/// - `origin`: A [`Rect`] representing the starting rectangle
38///   that is swept along a direction and length defined by the `delta` vector.
39/// - `target`: A [`Rect`] representing the target rectangle
40///   to check for intersection with the swept rectangle.
41/// - `delta`: The vector representing the movement direction and magnitude
42///   of the `origin` rectangle. This vector determines how far and in which
43///   direction the origin rectangle is moved.
44///
45/// # Returns
46///
47/// Returns `Some(RayIntersectionResult)` if there is an intersection between
48/// the swept rectangle and the target rectangle within the valid time range.
49///
50/// The [`RayIntersectionResult`] contains information about the intersection
51/// point and other related details. If there is no intersection or the
52/// intersection does not occur within the valid time range, `None` is returned.
53///
54/// # Example
55///
56/// ```rust
57/// use fixed32_math::{Rect, Vector};
58/// use impact_rs::prelude::*;
59///
60/// let origin = Rect::from((0.0, 0.0, 10.0, 10.0));
61/// let target = Rect::from((5.0, 5.0, 10.0, 10.0));
62/// let delta = Vector::from((15.0, 15.0));
63///
64/// match swept_rect_vs_rect(origin, target, delta) {
65///     Some(result) => {
66///         println!("Intersection found: {:?}", result);
67///     }
68///     None => {
69///         println!("No intersection.");
70///     }
71/// }
72/// ```
73#[must_use]
74pub fn swept_rect_vs_rect(
75    origin: Rect,
76    target: Rect,
77    delta: Vector,
78) -> Option<RayIntersectionResult> {
79    let expanded_target = Rect {
80        pos: target.pos - origin.size / 2,
81        size: target.size + origin.size,
82    };
83
84    let origin_point = origin.pos + origin.size;
85
86    let maybe_intersected = ray_vs_rect(origin_point, delta, expanded_target);
87    if let Some(result) = maybe_intersected {
88        let time = result.closest_time;
89        if time >= Fp::zero() && time < Fp::one() {
90            return Some(result);
91        }
92    }
93
94    None
95}
96
97/// Performs a ray-rectangle intersection test.
98///
99/// This function determines if a ray intersects with a given rectangle. The ray
100/// is defined by its origin and direction, and the rectangle is defined by its
101/// lower-left position and size. The function computes the intersection time and contact
102/// point if an intersection occurs.
103///
104/// # Parameters
105///
106/// - `ray_origin`: The origin point of the ray as a [`Vector`]. This is where
107///   the ray starts.
108/// - `ray_direction`: The ray as a [`Vector`]. This indicates
109///   the direction and length in which the ray is cast. The direction vector must not be zero.
110/// - `target`: The [`Rect`] representing the target rectangle to test for intersection.
111///   It is defined by its lower-left position and size.
112///
113/// # Returns
114///
115/// Returns `Some(RayIntersectionResult)` if there is an intersection between
116/// the ray and the rectangle. The [`RayIntersectionResult`] includes:
117/// - `contact_point`: The point of intersection between the ray and the rectangle.
118/// - `contact_normal`: The normal vector of the rectangle at the point of intersection.
119/// - `closest_time`: The normalized time along the ray at which the intersection occurs.
120///
121/// Returns `None` if there is no intersection or if the ray direction is zero.
122///
123/// # Example
124///
125/// ```rust
126/// use fixed32_math::{Rect, Vector};
127/// use impact_rs::prelude::*;
128///
129/// let ray_origin = Vector::from((0.0, 0.0));
130/// let ray_direction = Vector::from((1.0, 1.0));
131/// let target = Rect::from((5.0, 5.0, 10.0, 10.0));
132///
133/// match ray_vs_rect(ray_origin, ray_direction, target) {
134///     Some(result) => {
135///         println!("Intersection found at point: {:?}", result.contact_point);
136///         println!("Contact normal: {:?}", result.contact_normal);
137///         println!("Intersection time: {:?}", result.closest_time);
138///     }
139///     None => {
140///         println!("No intersection.");
141///     }
142/// }
143/// ```
144#[must_use]
145pub fn ray_vs_rect(
146    ray_origin: Vector,
147    ray_direction: Vector,
148    target: Rect,
149) -> Option<RayIntersectionResult> {
150    if ray_direction.x.is_zero() && ray_direction.y.is_zero() {
151        return None;
152    }
153
154    let mut time_near = Vector::default();
155    let mut time_far = Vector::default();
156
157    let inverted_direction = Vector::new(
158        if ray_direction.x != 0 {
159            Fp::one() / ray_direction.x
160        } else {
161            Fp::zero()
162        },
163        if ray_direction.y != 0 {
164            Fp::one() / ray_direction.y
165        } else {
166            Fp::zero()
167        },
168    );
169
170    match ray_direction.x.cmp(&Fp::zero()) {
171        Ordering::Greater => {
172            time_near.x = (target.pos.x - ray_origin.x) * inverted_direction.x;
173            time_far.x = (target.pos.x + target.size.x - ray_origin.x) * inverted_direction.x;
174        }
175        Ordering::Less => {
176            time_near.x = (target.pos.x + target.size.x - ray_origin.x) * inverted_direction.x;
177            time_far.x = (target.pos.x - ray_origin.x) * inverted_direction.x;
178        }
179        Ordering::Equal => {
180            // Ray direction is purely vertical
181            if ray_origin.x < target.pos.x || ray_origin.x > target.pos.x + target.size.x {
182                return None;
183            }
184            time_near.x = Fp::MIN;
185            time_far.x = Fp::MAX;
186        }
187    }
188
189    match ray_direction.y.cmp(&Fp::zero()) {
190        Ordering::Greater => {
191            time_near.y = (target.pos.y - ray_origin.y) * inverted_direction.y;
192            time_far.y = (target.pos.y + target.size.y - ray_origin.y) * inverted_direction.y;
193        }
194        Ordering::Less => {
195            time_near.y = (target.pos.y + target.size.y - ray_origin.y) * inverted_direction.y;
196            time_far.y = (target.pos.y - ray_origin.y) * inverted_direction.y;
197        }
198        Ordering::Equal => {
199            // Ray direction is purely horizontal
200            if ray_origin.y < target.pos.y || ray_origin.y > target.pos.y + target.size.y {
201                return None;
202            }
203            time_near.y = Fp::MIN;
204            time_far.y = Fp::MAX;
205        }
206    }
207
208    // Sort distances
209    if time_near.x > time_far.x {
210        std::mem::swap(&mut time_near.x, &mut time_far.x);
211    }
212
213    if time_near.y > time_far.y {
214        std::mem::swap(&mut time_near.y, &mut time_far.y);
215    }
216
217    if time_near.x >= time_far.y || time_near.y >= time_far.x {
218        return None;
219    }
220
221    let time_far_magnitude = min(time_far.x, time_far.y);
222
223    if time_far_magnitude < 0 {
224        return None;
225    }
226
227    let closest_time = max(time_near.x, time_near.y);
228
229    let contact_point = ray_origin + closest_time * ray_direction;
230
231    let mut contact_normal: Vector = Vector::default();
232
233    match time_near.x.cmp(&time_near.y) {
234        Ordering::Greater => {
235            contact_normal = if ray_direction.x > 0 {
236                Vector::right()
237            } else {
238                Vector::left()
239            };
240        }
241        Ordering::Less => {
242            contact_normal = if ray_direction.y > 0 {
243                Vector::up()
244            } else {
245                Vector::down()
246            };
247        }
248        Ordering::Equal => {
249            // Handle the case where time_near.x == time_near.y, if needed
250        }
251    }
252
253    Some(RayIntersectionResult {
254        contact_point,
255        contact_normal,
256        closest_time,
257    })
258}
259
260/// Checks for intersection between a vertically swept rectangle and a target rectangle.
261///
262/// This function determines if a rectangle, swept vertically from its initial
263/// position, intersects with a target rectangle. The swept rectangle is calculated
264/// based on the initial position and size of the origin rectangle, expanded to account
265/// for its vertical movement. The function performs a vertical intersection test
266/// to determine if there is an intersection within the valid time range.
267///
268/// # Parameters
269///
270/// - `origin`: A [`Rect`] representing the starting rectangle
271///   that is swept vertically.
272/// - `target`: A [`Rect`] representing the target rectangle
273///   to check for intersection with the swept rectangle.
274/// - `y_delta`: The vertical movement distance of the `origin` rectangle.
275///   This value indicates how far the origin rectangle moves along the y-axis.
276///
277/// # Returns
278///
279/// Returns `Some(Fp)` if there is an intersection between the swept rectangle
280/// and the target rectangle within the valid time range `[0, 1)`. The [`Fp`] value
281/// represents the normalized time at which the intersection occurs. If there is no intersection
282/// or if the intersection does not occur within the valid time range, `None` is returned.
283///
284/// # Example
285///
286/// ```rust
287/// use fixed32_math::{Rect, Vector};
288/// use impact_rs::prelude::*;
289/// use fixed32::Fp;
290///
291/// let origin = Rect::from((0.0, 0.0, 10.0, 10.0));
292/// let target = Rect::from((5.0, 15.0, 10.0, 10.0));
293/// let y_delta = Fp::from(20.0);
294///
295/// match swept_rect_vs_rect_vertical_time(origin, target, y_delta) {
296///     Some(time) => {
297///         println!("Intersection found at time: {:?}", time);
298///     }
299///     None => {
300///         println!("No intersection.");
301///     }
302/// }
303/// ```
304///
305#[must_use]
306pub fn swept_rect_vs_rect_vertical_time(origin: Rect, target: Rect, y_delta: Fp) -> Option<Fp> {
307    let combined_target_rect = Rect {
308        pos: target.pos,
309        size: target.size + origin.size,
310    };
311
312    let ray_origin = origin.pos + origin.size;
313
314    let maybe_intersected = ray_vs_rect_vertical_time(ray_origin, y_delta, combined_target_rect);
315    if let Some(time) = maybe_intersected {
316        if time >= Fp::zero() && time < Fp::one() {
317            return maybe_intersected;
318        }
319    }
320
321    None
322}
323
324/// Computes the intersection time of a vertical ray with a target rectangle.
325///
326/// This function calculates the time at which a vertical ray intersects a given
327/// rectangle. The ray is defined by its origin and its length of movement along
328/// the y-axis. The function determines if and when this ray intersects the vertical
329/// sides of the rectangle based on the ray's direction and position.
330///
331/// # Parameters
332///
333/// - `ray_origin`: The starting point of the ray in 2D space. This represents the
334///   position from which the ray begins.
335/// - `ray_length_in_y`: The length or direction of the ray's movement along the y-axis.
336///   A negative value indicates movement downward, while a positive value indicates
337///   movement upward.
338/// - `target_rect`: The rectangle with which the ray is tested for intersection.
339///   This rectangle is defined by its lower-left position and size.
340///
341/// # Returns
342///
343/// Returns `Some(Fp)` containing the intersection time if the ray intersects the
344/// target rectangle along the vertical axis. The returned [`Fp`] value represents the
345/// time at which the intersection occurs. If the ray does not intersect the rectangle
346/// or if it does not move vertically, `None` is returned.
347///
348/// # Example
349///
350/// ```rust
351/// use fixed32_math::{Rect, Vector};
352/// use impact_rs::prelude::*;
353/// use fixed32::Fp;
354///
355/// let ray_origin = Vector::from((5.0, 0.0));
356/// let ray_length_in_y = Fp::from(10.0);
357/// let target_rect = Rect::from((0.0, 5.0, 10.0, 10.0));
358///
359/// match ray_vs_rect_vertical_time(ray_origin, ray_length_in_y, target_rect) {
360///     Some(time) => {
361///         println!("Intersection occurs at time: {:?}", time);
362///     }
363///     None => {
364///         println!("No intersection or ray does not move vertically.");
365///     }
366/// }
367/// ```
368#[must_use]
369pub fn ray_vs_rect_vertical_time(
370    ray_origin: Vector,
371    ray_length_in_y: Fp,
372    target_rect: Rect,
373) -> Option<Fp> {
374    if ray_length_in_y.is_zero() {
375        return None;
376    }
377
378    if ray_origin.x < target_rect.pos.x || ray_origin.x > target_rect.pos.x + target_rect.size.x {
379        return None;
380    }
381
382    let closest_time = if ray_length_in_y > 0 {
383        (target_rect.pos.y - ray_origin.y) / ray_length_in_y
384    } else {
385        (target_rect.pos.y + target_rect.size.y - ray_origin.y) / ray_length_in_y
386    };
387
388    Some(closest_time)
389}
390
391/// Checks for intersection between a swept rectangle and a target rectangle
392/// along the horizontal axis.
393///
394/// This function determines if a rectangle, swept horizontally from its initial
395/// position, intersects with a target rectangle at any point during its horizontal
396/// movement. The swept rectangle is calculated based on the initial lower-left position and
397/// size of the origin rectangle, expanded to account for its movement along the
398/// x-axis. The function performs a horizontal ray-rectangle intersection test
399/// to check if there is an intersection within the valid time range.
400///
401/// # Parameters
402///
403/// - `origin`: A [`Rect`] representing the starting rectangle
404///   that is swept horizontally.
405/// - `target`: A [`Rect`] representing the target rectangle
406///   to check for intersection with the swept rectangle.
407/// - `x_delta`: The horizontal movement distance of the `origin` rectangle.
408///   This value represents how far the origin rectangle moves along the x-axis.
409///
410/// # Returns
411///
412/// Returns `Some(Fp)` if there is an intersection between the swept rectangle
413/// and the target rectangle along the horizontal axis within the valid time range.
414/// The `Fp` value represents the time at which the intersection occurs. If there
415/// is no intersection or if the intersection does not occur within the valid normalized time
416/// range `[0, 1)`, `None` is returned.
417///
418/// # Example
419///
420/// ```rust
421/// use fixed32_math::{Rect, Vector};
422/// use impact_rs::prelude::*;
423/// use fixed32::Fp;
424///
425/// let origin = Rect::from((0.0, 0.0, 10.0, 10.0));
426/// let target = Rect::from((15.0, 5.0, 10.0, 10.0));
427/// let x_delta = Fp::from(20.0);
428///
429/// match swept_rect_vs_rect_horizontal_time(origin, target, x_delta) {
430///     Some(time) => {
431///         println!("Intersection found at time: {:?}", time);
432///     }
433///     None => {
434///         println!("No intersection.");
435///     }
436/// }
437/// ```
438#[must_use]
439pub fn swept_rect_vs_rect_horizontal_time(origin: Rect, target: Rect, x_delta: Fp) -> Option<Fp> {
440    let expanded_target = Rect {
441        pos: target.pos,
442        size: target.size + origin.size,
443    };
444
445    let origin_point = origin.pos + origin.size;
446
447    let maybe_intersected = ray_vs_rect_horizontal_time(origin_point, x_delta, expanded_target);
448    if let Some(time) = maybe_intersected {
449        if time >= Fp::zero() && time < Fp::one() {
450            return maybe_intersected;
451        }
452    }
453
454    None
455}
456
457/// Computes the intersection time of a horizontal ray with a target rectangle.
458///
459/// This function calculates the time at which a horizontal ray intersects a given
460/// rectangle. The ray is defined by its origin and its length of movement along
461/// the x-axis. The function determines if and when this ray intersects the horizontal
462/// sides of the rectangle based on the ray's direction and position.
463///
464/// # Parameters
465///
466/// - `ray_origin`: The starting point of the ray in 2D space. This represents the
467///   position from which the ray begins.
468/// - `ray_length_in_x`: The length or direction of the ray's movement along the x-axis.
469///   A positive value indicates movement to the right, while a negative value indicates
470///   movement to the left.
471/// - `target_rect`: The rectangle with which the ray is tested for intersection.
472///   This rectangle is defined by its lower-left position and size.
473///
474/// # Returns
475///
476/// Returns `Some(Fp)` containing the intersection time if the ray intersects the
477/// target rectangle along the horizontal axis. The returned [`Fp`] value represents the
478/// time at which the intersection occurs. If the ray does not intersect the rectangle
479/// or if it does not move horizontally, `None` is returned.
480///
481/// # Example
482///
483/// ```rust
484/// use fixed32_math::{Rect, Vector};
485/// use impact_rs::prelude::*;
486/// use fixed32::Fp;
487///
488/// let ray_origin = Vector::from((0.0, 5.0));
489/// let ray_length_in_x = Fp::from(10.0);
490/// let target_rect = Rect::from((5.0, 0.0, 10.0, 10.0));
491///
492/// match ray_vs_rect_horizontal_time(ray_origin, ray_length_in_x, target_rect) {
493///     Some(time) => {
494///         println!("Intersection occurs at time: {:?}", time);
495///     }
496///     None => {
497///         println!("No intersection or ray does not move horizontally.");
498///     }
499/// }
500/// ```
501#[must_use]
502pub fn ray_vs_rect_horizontal_time(
503    ray_origin: Vector,
504    ray_length_in_x: Fp,
505    target_rect: Rect,
506) -> Option<Fp> {
507    if ray_length_in_x == 0 {
508        return None;
509    }
510
511    if ray_origin.y < target_rect.pos.y || ray_origin.y >= target_rect.pos.y + target_rect.size.y {
512        return None;
513    }
514
515    let closest_time = if ray_length_in_x > 0 {
516        (target_rect.pos.x - ray_origin.x) / ray_length_in_x
517    } else {
518        (target_rect.pos.x + target_rect.size.x - ray_origin.x) / ray_length_in_x
519    };
520
521    Some(closest_time)
522}