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}