From ba8821976d3dcb23c76778c4b8ba3e0687c0db21 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sun, 8 Dec 2024 03:25:19 +0200 Subject: [PATCH] Improve `SpatialQuery` APIs and docs, and add more configuration (#510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Fixes #458, among many issues that have come up in discussions. The API for shape casts through `SpatialQuery` is quite hard to read. If we supported all of Parry's configuration options for shape casts, `shape_hits` would end up looking like this: ```rust // Note: target_distance and compute_impact_on_penetration don't exist prior to this PR let hits = spatial.shape_hits( &shape, origin, shape_rotation, direction, max_time_of_impact, max_hits, target_distance, compute_impact_on_penetration, ignore_origin_penetration, &query_filter, ); ``` Without variables for everything, it would be almost entirely unreadable: ```rust let hits = spatial.shape_hits( &Collider::sphere(0.5), Vec3::ZERO, Quat::default(), Dir3::ZERO, 100.0, 10, 0.0, true, false, &default(), ); ``` Aside from bad ergonomics and poor readability, this is also bad for documentation, as the docs for each configuration option must be duplicated for each method that uses them. There are also no sane defaults, as *every* setting must be configured explicitly, even if the user is unlikely to need to care about some of them. This is especially bad for new users for whom the long list of arguments may be confusing and daunting. Another slightly unrelated issue is that many properties and docs use the term "time of impact", which has reportedly been a bit confusing for many users. In Avian's case, for ray casts and shape casts, it is actually just the distance travelled along the ray, because the cast direction is normalized. Most occurrences of "time of impact" should likely be renamed to just "distance", except for time-of-impact queries where both shapes are moving or there is angular velocity involved. ## Solution This PR fixes several API and documentation issues, along with doing general cleanup. It's a bit large with some slightly unrelated changes, so apologies in advance for potential reviewers! Shape casting methods now take a `ShapeCastConfig`. It holds general configuration options for the cast, and could be extended in the future to even have things like debug rendering options. The earlier example would now look like this: ```rust let hits = spatial.shape_hits( &shape, origin, shape_rotation, direction, max_hits, &ShapeCastConfig { max_distance, target_distance, compute_impact_on_penetration, ignore_origin_penetration, }, &filter, ); ``` In practice, you can often use default values for most properties, use constructor methods, and might move the configuration outside the method call (and even reuse it across shape casts), so it could actually look like this: ```rust let config = ShapeCastConfig::from_max_distance(100.0); let hits = spatial.shape_hits( &Collider::sphere(0.5), Vec3::ZERO, Quat::default(), Dir3::ZERO, 10, &config, &filter, ); ``` As you may have noticed, I also changed `max_time_of_impact` to `max_distance`. Most occurrences of "time of impact" have indeed been renamed to "distance" as per the reasons mentioned earlier. Additionally, I fixed several documentation issues and added missing methods and configuration options. See the changelog below for a slightly more thorough overview. --- ## Changelog - Added `ShapeCastConfig`. Most shape casting methods now take the config instead of many separate arguments - Renamed `RayHitData::time_of_impact`, `ShapeHitData::time_of_impact`, and many other occurrences of "time of impact" to `distance` - Added missing predicate versions of spatial query methods which existed on `SpatialQueryPipeline`, but not `SpatialQuery` - Added `target_distance` and `compute_impact_on_penetration` configuration options for shape casts, to match Parry's capabilities - Fixed several documentation issues, such as: - Ray and shape hit data is in *world space*, not local space - Many intra-doc for "See also" sections link to self, not other methods - Mentions of "ray" in shape casting docs - Confusing wording for `predicate` argument docs - Improved documentation examples - Improved clarity and readability in a lot of docs - Changed `excluded_entities` to use an `EntityHashSet` ## Migration Guide ### Shape Casting Configuration `SpatialQuery` methods like `cast_shape` and `shape_hits` now take a `ShapeCastConfig`, which contains a lot of the existing configuration options, along with a few new options. ```rust // Before let hits = spatial.shape_hits( &Collider::sphere(0.5), Vec3::ZERO, Quat::default(), Dir3::ZERO, 100.0, 10, false, &SpatialQueryFilter::from_mask(LayerMask::ALL), ); // After let hits = spatial.shape_hits( &Collider::sphere(0.5), Vec3::ZERO, Quat::default(), Dir3::ZERO, 10, &ShapeCastConfig::from_max_distance(100.0), &SpatialQueryFilter::from_mask(LayerMask::ALL), ); ``` ### Time of Impact → Distance Spatial query APIs that mention the "time of impact" have been changed to refer to "distance". This affects names of properties and methods, such as: - `RayCaster::max_time_of_impact` → `RayCaster::max_distance` - `RayCaster::with_max_time_of_impact` → `RayCaster::with_max_distance` - `ShapeCaster::max_time_of_impact` → `ShapeCaster::max_distance` - `ShapeCaster::with_max_time_of_impact` → `ShapeCaster::with_max_distance` - `RayHitData::time_of_impact` → `RayHitData::distance` - `ShapeHitData::time_of_impact` → `ShapeHitData::distance` - `max_time_of_impact` on `SpatialQuery` methods → `RayCastConfig::max_distance` or `ShapeCastConfig::max_distance` This was changed because "distance" is clearer than "time of impact" for many users, and it is still an accurate term, as the cast directions in Avian are always normalized, so the "velocity" is of unit length. --- .../examples/dynamic_character_2d/plugin.rs | 2 +- .../examples/kinematic_character_2d/plugin.rs | 2 +- crates/avian2d/examples/ray_caster.rs | 6 +- crates/avian3d/examples/cast_ray_predicate.rs | 33 +- .../examples/dynamic_character_3d/plugin.rs | 2 +- .../examples/kinematic_character_3d/plugin.rs | 2 +- src/collision/collider/parry/mod.rs | 16 +- src/debug_render/gizmos.rs | 28 +- src/debug_render/mod.rs | 4 +- src/picking/mod.rs | 6 +- src/spatial_query/mod.rs | 14 +- src/spatial_query/pipeline.rs | 330 ++++++----- src/spatial_query/query_filter.rs | 42 +- src/spatial_query/ray_caster.rs | 92 +-- src/spatial_query/shape_caster.rs | 217 +++++-- src/spatial_query/system_param.rs | 556 ++++++++++++------ 16 files changed, 852 insertions(+), 500 deletions(-) diff --git a/crates/avian2d/examples/dynamic_character_2d/plugin.rs b/crates/avian2d/examples/dynamic_character_2d/plugin.rs index b904e1e8..bb5b587b 100644 --- a/crates/avian2d/examples/dynamic_character_2d/plugin.rs +++ b/crates/avian2d/examples/dynamic_character_2d/plugin.rs @@ -106,7 +106,7 @@ impl CharacterControllerBundle { rigid_body: RigidBody::Dynamic, collider, ground_caster: ShapeCaster::new(caster_shape, Vector::ZERO, 0.0, Dir2::NEG_Y) - .with_max_time_of_impact(10.0), + .with_max_distance(10.0), locked_axes: LockedAxes::ROTATION_LOCKED, movement: MovementBundle::default(), } diff --git a/crates/avian2d/examples/kinematic_character_2d/plugin.rs b/crates/avian2d/examples/kinematic_character_2d/plugin.rs index 9888f20f..3e4f5c1c 100644 --- a/crates/avian2d/examples/kinematic_character_2d/plugin.rs +++ b/crates/avian2d/examples/kinematic_character_2d/plugin.rs @@ -120,7 +120,7 @@ impl CharacterControllerBundle { rigid_body: RigidBody::Kinematic, collider, ground_caster: ShapeCaster::new(caster_shape, Vector::ZERO, 0.0, Dir2::NEG_Y) - .with_max_time_of_impact(10.0), + .with_max_distance(10.0), gravity: ControllerGravity(gravity), movement: MovementBundle::default(), } diff --git a/crates/avian2d/examples/ray_caster.rs b/crates/avian2d/examples/ray_caster.rs index bbe5a218..3afca6cd 100644 --- a/crates/avian2d/examples/ray_caster.rs +++ b/crates/avian2d/examples/ray_caster.rs @@ -66,11 +66,7 @@ fn render_rays(mut rays: Query<(&mut RayCaster, &mut RayHits)>, mut gizmos: Gizm let direction = ray.global_direction().f32(); for hit in hits.iter() { - gizmos.line_2d( - origin, - origin + direction * hit.time_of_impact as f32, - GREEN, - ); + gizmos.line_2d(origin, origin + direction * hit.distance as f32, GREEN); } if hits.is_empty() { gizmos.line_2d(origin, origin + direction * 1_000_000.0, ORANGE_RED); diff --git a/crates/avian3d/examples/cast_ray_predicate.rs b/crates/avian3d/examples/cast_ray_predicate.rs index c566d17c..74a77ff4 100644 --- a/crates/avian3d/examples/cast_ray_predicate.rs +++ b/crates/avian3d/examples/cast_ray_predicate.rs @@ -145,38 +145,33 @@ fn raycast( query: SpatialQuery, mut materials: ResMut>, cubes: Query<(&MeshMaterial3d, &OutOfGlass)>, - mut indicator_transform: Query<&mut Transform, With>, + mut indicator_transform: Single<&mut Transform, With>, ) { let origin = Vector::new(-200.0, 2.0, 0.0); let direction = Dir3::X; + let filter = SpatialQueryFilter::default(); - let mut ray_indicator_transform = indicator_transform.single_mut(); - - if let Some(ray_hit_data) = query.cast_ray_predicate( - origin, - direction, - Scalar::MAX, - true, - &SpatialQueryFilter::default(), - &|entity| { + if let Some(ray_hit_data) = + query.cast_ray_predicate(origin, direction, Scalar::MAX, true, &filter, &|entity| { + // Only look at cubes not made out of glass. if let Ok((_, out_of_glass)) = cubes.get(entity) { - return !out_of_glass.0; // only look at cubes not out of glass + return !out_of_glass.0; } - true // if the collider has no OutOfGlass component, then check it nevertheless - }, - ) { - // set color of hit object to red + true + }) + { + // Set the color of the hit object to red. if let Ok((material_handle, _)) = cubes.get(ray_hit_data.entity) { if let Some(material) = materials.get_mut(material_handle) { material.base_color = RED.into(); } } - // set length of ray indicator to look more like a laser - let contact_point = (origin + direction.adjust_precision() * ray_hit_data.time_of_impact).x; + // Set the length of the ray indicator to look more like a laser, + let contact_point = (origin + direction.adjust_precision() * ray_hit_data.distance).x; let target_scale = 1000.0 + contact_point * 2.0; - ray_indicator_transform.scale.x = target_scale as f32; + indicator_transform.scale.x = target_scale as f32; } else { - ray_indicator_transform.scale.x = 2000.0; + indicator_transform.scale.x = 2000.0; } } diff --git a/crates/avian3d/examples/dynamic_character_3d/plugin.rs b/crates/avian3d/examples/dynamic_character_3d/plugin.rs index 207c3184..20bb222f 100644 --- a/crates/avian3d/examples/dynamic_character_3d/plugin.rs +++ b/crates/avian3d/examples/dynamic_character_3d/plugin.rs @@ -111,7 +111,7 @@ impl CharacterControllerBundle { Quaternion::default(), Dir3::NEG_Y, ) - .with_max_time_of_impact(0.2), + .with_max_distance(0.2), locked_axes: LockedAxes::ROTATION_LOCKED, movement: MovementBundle::default(), } diff --git a/crates/avian3d/examples/kinematic_character_3d/plugin.rs b/crates/avian3d/examples/kinematic_character_3d/plugin.rs index f542b48d..41f32268 100644 --- a/crates/avian3d/examples/kinematic_character_3d/plugin.rs +++ b/crates/avian3d/examples/kinematic_character_3d/plugin.rs @@ -126,7 +126,7 @@ impl CharacterControllerBundle { Quaternion::default(), Dir3::NEG_Y, ) - .with_max_time_of_impact(0.2), + .with_max_distance(0.2), gravity: ControllerGravity(gravity), movement: MovementBundle::default(), } diff --git a/src/collision/collider/parry/mod.rs b/src/collision/collider/parry/mod.rs index f81046fc..759e8db4 100644 --- a/src/collision/collider/parry/mod.rs +++ b/src/collision/collider/parry/mod.rs @@ -624,16 +624,16 @@ impl Collider { .contains_point(&make_isometry(translation, rotation), &point.into()) } - /// Computes the time of impact and normal between the given ray and `self` + /// Computes the distance and normal between the given ray and `self` /// transformed by `translation` and `rotation`. /// - /// The returned tuple is in the format `(time_of_impact, normal)`. + /// The returned tuple is in the format `(distance, normal)`. /// /// # Arguments /// /// - `ray_origin`: Where the ray is cast from. /// - `ray_direction`: What direction the ray is cast in. - /// - `max_time_of_impact`: The maximum distance that the ray can travel. + /// - `max_distance`: The maximum distance the ray can travel. /// - `solid`: If true and the ray origin is inside of a collider, the hit point will be the ray origin itself. /// Otherwise, the collider will be treated as hollow, and the hit point will be at the collider's boundary. pub fn cast_ray( @@ -642,13 +642,13 @@ impl Collider { rotation: impl Into, ray_origin: Vector, ray_direction: Vector, - max_time_of_impact: Scalar, + max_distance: Scalar, solid: bool, ) -> Option<(Scalar, Vector)> { let hit = self.shape_scaled().cast_ray_and_get_normal( &make_isometry(translation, rotation), &parry::query::Ray::new(ray_origin.into(), ray_direction.into()), - max_time_of_impact, + max_distance, solid, ); hit.map(|hit| (hit.time_of_impact, hit.normal.into())) @@ -660,19 +660,19 @@ impl Collider { /// /// - `ray_origin`: Where the ray is cast from. /// - `ray_direction`: What direction the ray is cast in. - /// - `max_time_of_impact`: The maximum distance that the ray can travel. + /// - `max_distance`: The maximum distance the ray can travel. pub fn intersects_ray( &self, translation: impl Into, rotation: impl Into, ray_origin: Vector, ray_direction: Vector, - max_time_of_impact: Scalar, + max_distance: Scalar, ) -> bool { self.shape_scaled().intersects_ray( &make_isometry(translation, rotation), &parry::query::Ray::new(ray_origin.into(), ray_direction.into()), - max_time_of_impact, + max_distance, ) } diff --git a/src/debug_render/gizmos.rs b/src/debug_render/gizmos.rs index 427b25d4..16dd9940 100644 --- a/src/debug_render/gizmos.rs +++ b/src/debug_render/gizmos.rs @@ -55,7 +55,7 @@ pub trait PhysicsGizmoExt { &mut self, origin: Vector, direction: Dir, - max_time_of_impact: Scalar, + max_distance: Scalar, hits: &[RayHitData], ray_color: Color, point_color: Color, @@ -75,7 +75,7 @@ pub trait PhysicsGizmoExt { origin: Vector, shape_rotation: impl Into, direction: Dir, - max_time_of_impact: Scalar, + max_distance: Scalar, hits: &[ShapeHitData], ray_color: Color, shape_color: Color, @@ -458,29 +458,29 @@ impl PhysicsGizmoExt for Gizmos<'_, '_, PhysicsGizmos> { &mut self, origin: Vector, direction: Dir, - max_time_of_impact: Scalar, + max_distance: Scalar, hits: &[RayHitData], ray_color: Color, point_color: Color, normal_color: Color, length_unit: Scalar, ) { - let max_toi = hits + let max_distance = hits .iter() - .max_by(|a, b| a.time_of_impact.total_cmp(&b.time_of_impact)) - .map_or(max_time_of_impact, |hit| hit.time_of_impact); + .max_by(|a, b| a.distance.total_cmp(&b.distance)) + .map_or(max_distance, |hit| hit.distance); // Draw ray as arrow self.draw_arrow( origin, - origin + direction.adjust_precision() * max_toi, + origin + direction.adjust_precision() * max_distance, 0.1 * length_unit, ray_color, ); // Draw all hit points and normals for hit in hits { - let point = origin + direction.adjust_precision() * hit.time_of_impact; + let point = origin + direction.adjust_precision() * hit.distance; // Draw hit point #[cfg(feature = "2d")] @@ -510,7 +510,7 @@ impl PhysicsGizmoExt for Gizmos<'_, '_, PhysicsGizmos> { origin: Vector, shape_rotation: impl Into, direction: Dir, - max_time_of_impact: Scalar, + max_distance: Scalar, hits: &[ShapeHitData], ray_color: Color, shape_color: Color, @@ -522,10 +522,10 @@ impl PhysicsGizmoExt for Gizmos<'_, '_, PhysicsGizmos> { #[cfg(feature = "3d")] let shape_rotation = Rotation(shape_rotation.normalize()); - let max_toi = hits + let max_distance = hits .iter() - .max_by(|a, b| a.time_of_impact.total_cmp(&b.time_of_impact)) - .map_or(max_time_of_impact, |hit| hit.time_of_impact); + .max_by(|a, b| a.distance.total_cmp(&b.distance)) + .map_or(max_distance, |hit| hit.distance); // Draw collider at origin self.draw_collider(shape, origin, shape_rotation, shape_color); @@ -534,7 +534,7 @@ impl PhysicsGizmoExt for Gizmos<'_, '_, PhysicsGizmos> { // TODO: We could render the swept collider outline instead self.draw_arrow( origin, - origin + max_toi * direction.adjust_precision(), + origin + max_distance * direction.adjust_precision(), 0.1 * length_unit, ray_color, ); @@ -558,7 +558,7 @@ impl PhysicsGizmoExt for Gizmos<'_, '_, PhysicsGizmos> { // Draw collider at hit point self.draw_collider( shape, - origin + hit.time_of_impact * direction.adjust_precision(), + origin + hit.distance * direction.adjust_precision(), shape_rotation, shape_color.with_alpha(0.3), ); diff --git a/src/debug_render/mod.rs b/src/debug_render/mod.rs index a6c342f0..954d09ea 100644 --- a/src/debug_render/mod.rs +++ b/src/debug_render/mod.rs @@ -426,7 +426,7 @@ fn debug_render_raycasts( ray.global_origin(), ray.global_direction(), // f32::MAX renders nothing, but this number seems to be fine :P - ray.max_time_of_impact.min(1_000_000_000_000_000_000.0), + ray.max_distance.min(1_000_000_000_000_000_000.0), hits.as_slice(), ray_color, point_color, @@ -459,7 +459,7 @@ fn debug_render_shapecasts( shape_caster.global_shape_rotation(), shape_caster.global_direction(), // f32::MAX renders nothing, but this number seems to be fine :P - shape_caster.max_time_of_impact.min(1_000_000_000_000_000.0), + shape_caster.max_distance.min(1_000_000_000_000_000.0), hits.as_slice(), ray_color, shape_color, diff --git a/src/picking/mod.rs b/src/picking/mod.rs index 8601757a..46483972 100644 --- a/src/picking/mod.rs +++ b/src/picking/mod.rs @@ -133,11 +133,11 @@ pub fn update_hits( ) .map(|ray_hit_data| { #[allow(clippy::unnecessary_cast)] - let toi = ray_hit_data.time_of_impact as f32; + let distance = ray_hit_data.distance as f32; let hit_data = HitData::new( ray_id.camera, - toi, - Some(ray.origin + *ray.direction * toi), + distance, + Some(ray.get_point(distance)), Some(ray_hit_data.normal.f32()), ); (ray_hit_data.entity, hit_data) diff --git a/src/spatial_query/mod.rs b/src/spatial_query/mod.rs index 03b6375a..be15ce16 100644 --- a/src/spatial_query/mod.rs +++ b/src/spatial_query/mod.rs @@ -20,9 +20,8 @@ //! a variety of things like getting information about the environment for character controllers and AI, //! and even rendering using ray tracing. //! -//! For each hit during raycasting, the hit entity, a *time of impact* and a normal will be stored in [`RayHitData`]. -//! The time of impact refers to how long the ray travelled, which is essentially the distance from the ray origin to -//! the point of intersection. +//! For each hit during raycasting, the hit entity, a distance, and a normal will be stored in [`RayHitData`]. +//! The distance is the distance from the ray origin to the point of intersection, indicating how far the ray travelled. //! //! There are two ways to perform raycasts. //! @@ -59,7 +58,7 @@ //! println!( //! "Hit entity {} at {} with normal {}", //! hit.entity, -//! ray.origin + *ray.direction * hit.time_of_impact, +//! ray.origin + *ray.direction * hit.distance, //! hit.normal, //! ); //! } @@ -76,9 +75,8 @@ //! we have an entire shape travelling along a half-line. One use case is determining how far an object can move //! before it hits the environment. //! -//! For each hit during shapecasting, the hit entity, the *time of impact*, two local points of intersection and two local -//! normals will be stored in [`ShapeHitData`]. The time of impact refers to how long the shape travelled before the initial -//! hit, which is essentially the distance from the shape origin to the global point of intersection. +//! For each hit during shapecasting, the hit entity, a distance, two world-space points of intersection and two world-space +//! normals will be stored in [`ShapeHitData`]. The distance refers to how far the shape travelled before the initial hit. //! //! There are two ways to perform shapecasts. //! @@ -107,7 +105,7 @@ //! Collider::sphere(0.5), // Shape //! Vec3::ZERO, // Origin //! Quat::default(), // Shape rotation -//! Dir3::X // Direction +//! Dir3::X // Direction //! )); //! // ...spawn colliders and other things //! } diff --git a/src/spatial_query/pipeline.rs b/src/spatial_query/pipeline.rs index 79ce94bb..125c0787 100644 --- a/src/spatial_query/pipeline.rs +++ b/src/spatial_query/pipeline.rs @@ -155,28 +155,25 @@ impl SpatialQueryPipeline { /// /// - `origin`: Where the ray is cast from. /// - `direction`: What direction the ray is cast in. - /// - `max_time_of_impact`: The maximum distance that the ray can travel. - /// - `solid`: If true and the ray origin is inside of a collider, the hit point will be the ray origin itself. - /// Otherwise, the collider will be treated as hollow, and the hit point will be at the collider's boundary. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `max_distance`: The maximum distance the ray can travel. + /// - `solid`: If true *and* the ray origin is inside of a collider, the hit point will be the ray origin itself. + /// Otherwise, the collider will be treated as hollow, and the hit point will be at its boundary. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// - /// See also: [`SpatialQuery::cast_ray`] + /// # Related Methods + /// + /// - [`SpatialQueryPipeline::cast_ray_predicate`] + /// - [`SpatialQueryPipeline::ray_hits`] + /// - [`SpatialQueryPipeline::ray_hits_callback`] pub fn cast_ray( &self, origin: Vector, direction: Dir, - max_time_of_impact: Scalar, + max_distance: Scalar, solid: bool, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, ) -> Option { - self.cast_ray_predicate( - origin, - direction, - max_time_of_impact, - solid, - query_filter, - &|_| true, - ) + self.cast_ray_predicate(origin, direction, max_distance, solid, filter, &|_| true) } /// Casts a [ray](spatial_query#raycasting) and computes the closest [hit](RayHitData) with a collider. @@ -186,29 +183,32 @@ impl SpatialQueryPipeline { /// /// - `origin`: Where the ray is cast from. /// - `direction`: What direction the ray is cast in. - /// - `max_time_of_impact`: The maximum distance that the ray can travel. - /// - `solid`: If true and the ray origin is inside of a collider, the hit point will be the ray origin itself. - /// Otherwise, the collider will be treated as hollow, and the hit point will be at the collider's boundary. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. - /// - `predicate`: A function with which the colliders are filtered. Given the Entity it should return false, if the - /// entity should be ignored. - /// - /// See also: [`SpatialQuery::cast_ray`] + /// - `max_distance`: The maximum distance the ray can travel. + /// - `solid`: If true *and* the ray origin is inside of a collider, the hit point will be the ray origin itself. + /// Otherwise, the collider will be treated as hollow, and the hit point will be at its boundary. + /// - `predicate`: A function called on each entity hit by the ray. The ray keeps travelling until the predicate returns `false`. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// + /// # Related Methods + /// + /// - [`SpatialQueryPipeline::cast_ray`] + /// - [`SpatialQueryPipeline::ray_hits`] + /// - [`SpatialQueryPipeline::ray_hits_callback`] pub fn cast_ray_predicate( &self, origin: Vector, direction: Dir, - max_time_of_impact: Scalar, + max_distance: Scalar, solid: bool, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, predicate: &dyn Fn(Entity) -> bool, ) -> Option { - let pipeline_shape = self.as_composite_shape_with_predicate(query_filter, predicate); + let pipeline_shape = self.as_composite_shape_with_predicate(filter, predicate); let ray = parry::query::Ray::new(origin.into(), direction.adjust_precision().into()); let mut visitor = RayCompositeShapeToiAndNormalBestFirstVisitor::new( &pipeline_shape, &ray, - max_time_of_impact, + max_distance, solid, ); @@ -216,7 +216,7 @@ impl SpatialQueryPipeline { .traverse_best_first(&mut visitor) .map(|(_, (entity_index, hit))| RayHitData { entity: self.entity_from_index(entity_index), - time_of_impact: hit.time_of_impact, + distance: hit.time_of_impact, normal: hit.normal.into(), }) } @@ -230,34 +230,31 @@ impl SpatialQueryPipeline { /// /// - `origin`: Where the ray is cast from. /// - `direction`: What direction the ray is cast in. - /// - `max_time_of_impact`: The maximum distance that the ray can travel. + /// - `max_distance`: The maximum distance the ray can travel. /// - `max_hits`: The maximum number of hits. Additional hits will be missed. - /// - `solid`: If true and the ray origin is inside of a collider, the hit point will be the ray origin itself. - /// Otherwise, the collider will be treated as hollow, and the hit point will be at the collider's boundary. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `solid`: If true *and* the ray origin is inside of a collider, the hit point will be the ray origin itself. + /// Otherwise, the collider will be treated as hollow, and the hit point will be at its boundary. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// + /// # Related Methods /// - /// See also: [`SpatialQuery::ray_hits`] + /// - [`SpatialQueryPipeline::cast_ray`] + /// - [`SpatialQueryPipeline::cast_ray_predicate`] + /// - [`SpatialQueryPipeline::ray_hits_callback`] pub fn ray_hits( &self, origin: Vector, direction: Dir, - max_time_of_impact: Scalar, + max_distance: Scalar, max_hits: u32, solid: bool, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, ) -> Vec { let mut hits = Vec::with_capacity(10); - self.ray_hits_callback( - origin, - direction, - max_time_of_impact, - solid, - query_filter, - |hit| { - hits.push(hit); - (hits.len() as u32) < max_hits - }, - ); + self.ray_hits_callback(origin, direction, max_distance, solid, filter, |hit| { + hits.push(hit); + (hits.len() as u32) < max_hits + }); hits } @@ -270,20 +267,24 @@ impl SpatialQueryPipeline { /// /// - `origin`: Where the ray is cast from. /// - `direction`: What direction the ray is cast in. - /// - `max_time_of_impact`: The maximum distance that the ray can travel. - /// - `solid`: If true and the ray origin is inside of a collider, the hit point will be the ray origin itself. - /// Otherwise, the collider will be treated as hollow, and the hit point will be at the collider's boundary. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `max_distance`: The maximum distance the ray can travel. + /// - `solid`: If true *and* the ray origin is inside of a collider, the hit point will be the ray origin itself. + /// Otherwise, the collider will be treated as hollow, and the hit point will be at its boundary. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// - `callback`: A callback function called for each hit. /// - /// See also: [`SpatialQuery::ray_hits_callback`] + /// # Related Methods + /// + /// - [`SpatialQueryPipeline::cast_ray`] + /// - [`SpatialQueryPipeline::cast_ray_predicate`] + /// - [`SpatialQueryPipeline::ray_hits`] pub fn ray_hits_callback( &self, origin: Vector, direction: Dir, - max_time_of_impact: Scalar, + max_distance: Scalar, solid: bool, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, mut callback: impl FnMut(RayHitData) -> bool, ) { let colliders = &self.colliders; @@ -293,16 +294,15 @@ impl SpatialQueryPipeline { let mut leaf_callback = &mut |entity_index: &u32| { let entity = self.entity_from_index(*entity_index); if let Some((iso, shape, layers)) = colliders.get(&entity) { - if query_filter.test(entity, *layers) { - if let Some(hit) = shape.shape_scaled().cast_ray_and_get_normal( - iso, - &ray, - max_time_of_impact, - solid, - ) { + if filter.test(entity, *layers) { + if let Some(hit) = + shape + .shape_scaled() + .cast_ray_and_get_normal(iso, &ray, max_distance, solid) + { let hit = RayHitData { entity, - time_of_impact: hit.time_of_impact, + distance: hit.time_of_impact, normal: hit.normal.into(), }; @@ -313,8 +313,7 @@ impl SpatialQueryPipeline { true }; - let mut visitor = - RayIntersectionsVisitor::new(&ray, max_time_of_impact, &mut leaf_callback); + let mut visitor = RayIntersectionsVisitor::new(&ray, max_distance, &mut leaf_callback); self.qbvh.traverse_depth_first(&mut visitor); } @@ -329,13 +328,14 @@ impl SpatialQueryPipeline { /// - `origin`: Where the shape is cast from. /// - `shape_rotation`: The rotation of the shape being cast. /// - `direction`: What direction the shape is cast in. - /// - `max_time_of_impact`: The maximum distance that the shape can travel. - /// - `ignore_origin_penetration`: If true and the shape is already penetrating a collider at the - /// shape origin, the hit will be ignored and only the next hit will be computed. Otherwise, the initial - /// hit will be returned. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `config`: A [`ShapeCastConfig`] that determines the behavior of the cast. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// - /// See also: [`SpatialQuery::cast_shape`] + /// # Related Methods + /// + /// - [`SpatialQueryPipeline::cast_shape_predicate`] + /// - [`SpatialQueryPipeline::shape_hits`] + /// - [`SpatialQueryPipeline::shape_hits_callback`] #[allow(clippy::too_many_arguments)] pub fn cast_shape( &self, @@ -343,18 +343,16 @@ impl SpatialQueryPipeline { origin: Vector, shape_rotation: RotationValue, direction: Dir, - max_time_of_impact: Scalar, - ignore_origin_penetration: bool, - query_filter: &SpatialQueryFilter, + config: &ShapeCastConfig, + filter: &SpatialQueryFilter, ) -> Option { self.cast_shape_predicate( shape, origin, shape_rotation, direction, - max_time_of_impact, - ignore_origin_penetration, - query_filter, + config, + filter, &|_| true, ) } @@ -370,15 +368,15 @@ impl SpatialQueryPipeline { /// - `origin`: Where the shape is cast from. /// - `shape_rotation`: The rotation of the shape being cast. /// - `direction`: What direction the shape is cast in. - /// - `max_time_of_impact`: The maximum distance that the shape can travel. - /// - `ignore_origin_penetration`: If true and the shape is already penetrating a collider at the - /// shape origin, the hit will be ignored and only the next hit will be computed. Otherwise, the initial - /// hit will be returned. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. - /// - `predicate`: A function with which the colliders are filtered. Given the Entity it should return false, if the - /// entity should be ignored. - /// - /// See also: [`SpatialQuery::cast_shape`] + /// - `config`: A [`ShapeCastConfig`] that determines the behavior of the cast. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `predicate`: A function called on each entity hit by the shape. The shape keeps travelling until the predicate returns `false`. + /// + /// # Related Methods + /// + /// - [`SpatialQueryPipeline::cast_shape`] + /// - [`SpatialQueryPipeline::shape_hits`] + /// - [`SpatialQueryPipeline::shape_hits_callback`] #[allow(clippy::too_many_arguments)] pub fn cast_shape_predicate( &self, @@ -386,9 +384,8 @@ impl SpatialQueryPipeline { origin: Vector, shape_rotation: RotationValue, direction: Dir, - max_time_of_impact: Scalar, - ignore_origin_penetration: bool, - query_filter: &SpatialQueryFilter, + config: &ShapeCastConfig, + filter: &SpatialQueryFilter, predicate: &dyn Fn(Entity) -> bool, ) -> Option { let rotation: Rotation; @@ -403,7 +400,7 @@ impl SpatialQueryPipeline { let shape_isometry = make_isometry(origin, rotation); let shape_direction = direction.adjust_precision().into(); - let pipeline_shape = self.as_composite_shape_with_predicate(query_filter, predicate); + let pipeline_shape = self.as_composite_shape_with_predicate(filter, predicate); let mut visitor = TOICompositeShapeShapeBestFirstVisitor::new( &*self.dispatcher, &shape_isometry, @@ -411,8 +408,9 @@ impl SpatialQueryPipeline { &pipeline_shape, &**shape.shape_scaled(), ShapeCastOptions { - max_time_of_impact, - stop_at_penetration: !ignore_origin_penetration, + max_time_of_impact: config.max_distance, + stop_at_penetration: !config.ignore_origin_penetration, + compute_impact_geometry_on_penetration: config.compute_contact_on_penetration, ..default() }, ); @@ -421,7 +419,7 @@ impl SpatialQueryPipeline { .traverse_best_first(&mut visitor) .map(|(_, (entity_index, hit))| ShapeHitData { entity: self.entity_from_index(entity_index), - time_of_impact: hit.time_of_impact, + distance: hit.time_of_impact, point1: hit.witness1.into(), point2: hit.witness2.into(), normal1: hit.normal1.into(), @@ -430,7 +428,7 @@ impl SpatialQueryPipeline { } /// Casts a [shape](spatial_query#shapecasting) with a given rotation and computes computes all [hits](ShapeHitData) - /// in the order of the time of impact until `max_hits` is reached. + /// in the order of distance until `max_hits` is reached. /// /// # Arguments /// @@ -438,15 +436,15 @@ impl SpatialQueryPipeline { /// - `origin`: Where the shape is cast from. /// - `shape_rotation`: The rotation of the shape being cast. /// - `direction`: What direction the shape is cast in. - /// - `max_time_of_impact`: The maximum distance that the shape can travel. /// - `max_hits`: The maximum number of hits. Additional hits will be missed. - /// - `ignore_origin_penetration`: If true and the shape is already penetrating a collider at the - /// shape origin, the hit will be ignored and only the next hit will be computed. Otherwise, the initial - /// hit will be returned. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. - /// - `callback`: A callback function called for each hit. + /// - `config`: A [`ShapeCastConfig`] that determines the behavior of the cast. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// - /// See also: [`SpatialQuery::shape_hits`] + /// # Related Methods + /// + /// - [`SpatialQueryPipeline::cast_shape`] + /// - [`SpatialQueryPipeline::cast_shape_predicate`] + /// - [`SpatialQueryPipeline::shape_hits_callback`] #[allow(clippy::too_many_arguments)] pub fn shape_hits( &self, @@ -454,10 +452,9 @@ impl SpatialQueryPipeline { origin: Vector, shape_rotation: RotationValue, direction: Dir, - max_time_of_impact: Scalar, max_hits: u32, - ignore_origin_penetration: bool, - query_filter: &SpatialQueryFilter, + config: &ShapeCastConfig, + filter: &SpatialQueryFilter, ) -> Vec { let mut hits = Vec::with_capacity(10); self.shape_hits_callback( @@ -465,9 +462,8 @@ impl SpatialQueryPipeline { origin, shape_rotation, direction, - max_time_of_impact, - ignore_origin_penetration, - query_filter, + config, + filter, |hit| { hits.push(hit); (hits.len() as u32) < max_hits @@ -477,7 +473,7 @@ impl SpatialQueryPipeline { } /// Casts a [shape](spatial_query#shapecasting) with a given rotation and computes computes all [hits](ShapeHitData) - /// in the order of the time of impact, calling the given `callback` for each hit. The shapecast stops when + /// in the order of distance, calling the given `callback` for each hit. The shapecast stops when /// `callback` returns false or all hits have been found. /// /// # Arguments @@ -486,14 +482,15 @@ impl SpatialQueryPipeline { /// - `origin`: Where the shape is cast from. /// - `shape_rotation`: The rotation of the shape being cast. /// - `direction`: What direction the shape is cast in. - /// - `max_time_of_impact`: The maximum distance that the shape can travel. - /// - `ignore_origin_penetration`: If true and the shape is already penetrating a collider at the - /// shape origin, the hit will be ignored and only the next hit will be computed. Otherwise, the initial - /// hit will be returned. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `config`: A [`ShapeCastConfig`] that determines the behavior of the cast. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// - `callback`: A callback function called for each hit. /// - /// See also: [`SpatialQuery::shape_hits_callback`] + /// # Related Methods + /// + /// - [`SpatialQueryPipeline::cast_shape`] + /// - [`SpatialQueryPipeline::cast_shape_predicate`] + /// - [`SpatialQueryPipeline::shape_hits`] #[allow(clippy::too_many_arguments)] pub fn shape_hits_callback( &self, @@ -501,15 +498,21 @@ impl SpatialQueryPipeline { origin: Vector, shape_rotation: RotationValue, direction: Dir, - max_time_of_impact: Scalar, - ignore_origin_penetration: bool, - query_filter: &SpatialQueryFilter, + config: &ShapeCastConfig, + filter: &SpatialQueryFilter, mut callback: impl FnMut(ShapeHitData) -> bool, ) { // TODO: This clone is here so that the excluded entities in the original `query_filter` aren't modified. // We could remove this if shapecasting could compute multiple hits without just doing casts in a loop. // See https://github.com/Jondolf/avian/issues/403. - let mut query_filter = query_filter.clone(); + let mut query_filter = filter.clone(); + + let shape_cast_options = ShapeCastOptions { + max_time_of_impact: config.max_distance, + target_distance: config.target_distance, + stop_at_penetration: !config.ignore_origin_penetration, + compute_impact_geometry_on_penetration: config.compute_contact_on_penetration, + }; let rotation: Rotation; #[cfg(feature = "2d")] @@ -532,11 +535,7 @@ impl SpatialQueryPipeline { &shape_direction, &pipeline_shape, &**shape.shape_scaled(), - ShapeCastOptions { - max_time_of_impact, - stop_at_penetration: !ignore_origin_penetration, - ..default() - }, + shape_cast_options, ); if let Some(hit) = @@ -544,7 +543,7 @@ impl SpatialQueryPipeline { .traverse_best_first(&mut visitor) .map(|(_, (entity_index, hit))| ShapeHitData { entity: self.entity_from_index(entity_index), - time_of_impact: hit.time_of_impact, + distance: hit.time_of_impact, point1: hit.witness1.into(), point2: hit.witness2.into(), normal1: hit.normal1.into(), @@ -570,16 +569,18 @@ impl SpatialQueryPipeline { /// - `point`: The point that should be projected. /// - `solid`: If true and the point is inside of a collider, the projection will be at the point. /// Otherwise, the collider will be treated as hollow, and the projection will be at the collider's boundary. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// - /// See also: [`SpatialQuery::project_point`] + /// # Related Methods + /// + /// - [`SpatialQueryPipeline::project_point_predicate`] pub fn project_point( &self, point: Vector, solid: bool, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, ) -> Option { - self.project_point_predicate(point, solid, query_filter, &|_| true) + self.project_point_predicate(point, solid, filter, &|_| true) } /// Finds the [projection](spatial_query#point-projection) of a given point on the closest [collider](Collider). @@ -590,20 +591,21 @@ impl SpatialQueryPipeline { /// - `point`: The point that should be projected. /// - `solid`: If true and the point is inside of a collider, the projection will be at the point. /// Otherwise, the collider will be treated as hollow, and the projection will be at the collider's boundary. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. - /// - `predicate`: A function with which the colliders are filtered. Given the Entity it should return false, if the - /// entity should be ignored. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `predicate`: A function for filtering which entities are considered in the query. The projection will be on the closest collider that passes the predicate. + /// + /// # Related Methods /// - /// See also: [`SpatialQuery::project_point`] + /// - [`SpatialQueryPipeline::project_point`] pub fn project_point_predicate( &self, point: Vector, solid: bool, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, predicate: &dyn Fn(Entity) -> bool, ) -> Option { let point = point.into(); - let pipeline_shape = self.as_composite_shape_with_predicate(query_filter, predicate); + let pipeline_shape = self.as_composite_shape_with_predicate(filter, predicate); let mut visitor = PointCompositeShapeProjBestFirstVisitor::new(&pipeline_shape, &point, solid); @@ -622,16 +624,14 @@ impl SpatialQueryPipeline { /// # Arguments /// /// - `point`: The point that intersections are tested against. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// - /// See also: [`SpatialQuery::point_intersections`] - pub fn point_intersections( - &self, - point: Vector, - query_filter: &SpatialQueryFilter, - ) -> Vec { + /// # Related Methods + /// + /// - [`SpatialQueryPipeline::point_intersections_callback`] + pub fn point_intersections(&self, point: Vector, filter: &SpatialQueryFilter) -> Vec { let mut intersections = vec![]; - self.point_intersections_callback(point, query_filter, |e| { + self.point_intersections_callback(point, filter, |e| { intersections.push(e); true }); @@ -645,14 +645,16 @@ impl SpatialQueryPipeline { /// # Arguments /// /// - `point`: The point that intersections are tested against. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// - `callback`: A callback function called for each intersection. /// - /// See also: [`SpatialQuery::point_intersections_callback`] + /// # Related Methods + /// + /// - [`SpatialQueryPipeline::point_intersections`] pub fn point_intersections_callback( &self, point: Vector, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, mut callback: impl FnMut(Entity) -> bool, ) { let point = point.into(); @@ -660,7 +662,7 @@ impl SpatialQueryPipeline { let mut leaf_callback = &mut |entity_index: &u32| { let entity = self.entity_from_index(*entity_index); if let Some((isometry, shape, layers)) = self.colliders.get(&entity) { - if query_filter.test(entity, *layers) + if filter.test(entity, *layers) && shape.shape_scaled().contains_point(isometry, &point) { return callback(entity); @@ -676,7 +678,9 @@ impl SpatialQueryPipeline { /// An [intersection test](spatial_query#intersection-tests) that finds all entities with a [`ColliderAabb`] /// that is intersecting the given `aabb`. /// - /// See also: [`SpatialQuery::point_intersections_callback`] + /// # Related Methods + /// + /// - [`SpatialQueryPipeline::aabb_intersections_with_aabb_callback`] pub fn aabb_intersections_with_aabb(&self, aabb: ColliderAabb) -> Vec { let mut intersections = vec![]; self.aabb_intersections_with_aabb_callback(aabb, |e| { @@ -690,7 +694,9 @@ impl SpatialQueryPipeline { /// that is intersecting the given `aabb`, calling `callback` for each intersection. /// The search stops when `callback` returns `false` or all intersections have been found. /// - /// See also: [`SpatialQuery::aabb_intersections_with_aabb_callback`] + /// # Related Methods + /// + /// - [`SpatialQueryPipeline::aabb_intersections_with_aabb`] pub fn aabb_intersections_with_aabb_callback( &self, aabb: ColliderAabb, @@ -719,27 +725,23 @@ impl SpatialQueryPipeline { /// - `shape`: The shape that intersections are tested against represented as a [`Collider`]. /// - `shape_position`: The position of the shape. /// - `shape_rotation`: The rotation of the shape. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// + /// # Related Methods /// - /// See also: [`SpatialQuery::shape_intersections`] + /// - [`SpatialQueryPipeline::shape_intersections_callback`] pub fn shape_intersections( &self, shape: &Collider, shape_position: Vector, shape_rotation: RotationValue, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, ) -> Vec { let mut intersections = vec![]; - self.shape_intersections_callback( - shape, - shape_position, - shape_rotation, - query_filter, - |e| { - intersections.push(e); - true - }, - ); + self.shape_intersections_callback(shape, shape_position, shape_rotation, filter, |e| { + intersections.push(e); + true + }); intersections } @@ -752,16 +754,18 @@ impl SpatialQueryPipeline { /// - `shape`: The shape that intersections are tested against represented as a [`Collider`]. /// - `shape_position`: The position of the shape. /// - `shape_rotation`: The rotation of the shape. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// - `callback`: A callback function called for each intersection. /// - /// See also: [`SpatialQuery::shape_intersections_callback`] + /// # Related Methods + /// + /// - [`SpatialQueryPipeline::shape_intersections`] pub fn shape_intersections_callback( &self, shape: &Collider, shape_position: Vector, shape_rotation: RotationValue, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, mut callback: impl FnMut(Entity) -> bool, ) { let colliders = &self.colliders; @@ -784,7 +788,7 @@ impl SpatialQueryPipeline { let entity = self.entity_from_index(*entity_index); if let Some((collider_isometry, collider, layers)) = colliders.get(&entity) { - if query_filter.test(entity, *layers) { + if filter.test(entity, *layers) { let isometry = inverse_shape_isometry * collider_isometry; if dispatcher.intersection_test( diff --git a/src/spatial_query/query_filter.rs b/src/spatial_query/query_filter.rs index d0273cb4..b4d4abb0 100644 --- a/src/spatial_query/query_filter.rs +++ b/src/spatial_query/query_filter.rs @@ -1,4 +1,7 @@ -use bevy::{prelude::*, utils::HashSet}; +use bevy::{ + ecs::entity::{EntityHash, EntityHashSet}, + prelude::*, +}; use crate::prelude::*; @@ -36,21 +39,28 @@ pub struct SpatialQueryFilter { /// Specifies which [collision layers](CollisionLayers) will be included in the [spatial query](crate::spatial_query). pub mask: LayerMask, /// Entities that will not be included in [spatial queries](crate::spatial_query). - pub excluded_entities: HashSet, + pub excluded_entities: EntityHashSet, } impl Default for SpatialQueryFilter { fn default() -> Self { - Self { - mask: LayerMask::ALL, - excluded_entities: default(), - } + Self::DEFAULT } } impl SpatialQueryFilter { + /// The default [`SpatialQueryFilter`] configuration that includes all collision layers + /// and has no excluded entities. + pub const DEFAULT: Self = Self { + mask: LayerMask::ALL, + excluded_entities: EntityHashSet::with_hasher(EntityHash), + }; + /// Creates a new [`SpatialQueryFilter`] with the given [`LayerMask`] determining - /// which [collision layers](CollisionLayers) will be included in the [spatial query](crate::spatial_query). + /// which [collision layers] will be included in the [spatial query]. + /// + /// [collision layers]: CollisionLayers + /// [spatial query]: crate::spatial_query pub fn from_mask(mask: impl Into) -> Self { Self { mask: mask.into(), @@ -58,16 +68,21 @@ impl SpatialQueryFilter { } } - /// Creates a new [`SpatialQueryFilter`] with the given entities excluded from the [spatial query](crate::spatial_query). + /// Creates a new [`SpatialQueryFilter`] with the given entities excluded from the [spatial query]. + /// + /// [spatial query]: crate::spatial_query pub fn from_excluded_entities(entities: impl IntoIterator) -> Self { Self { - excluded_entities: HashSet::from_iter(entities), + excluded_entities: EntityHashSet::from_iter(entities), ..default() } } /// Sets the [`LayerMask`] of the filter configuration. Only colliders with the corresponding - /// [collision layer memberships](CollisionLayers) will be included in the [spatial query](crate::spatial_query). + /// [collision layer memberships] will be included in the [spatial query]. + /// + /// [collision layer memberships]: CollisionLayers + /// [spatial query]: crate::spatial_query pub fn with_mask(mut self, masks: impl Into) -> Self { self.mask = masks.into(); self @@ -75,12 +90,13 @@ impl SpatialQueryFilter { /// Excludes the given entities from the [spatial query](crate::spatial_query). pub fn with_excluded_entities(mut self, entities: impl IntoIterator) -> Self { - self.excluded_entities = HashSet::from_iter(entities); + self.excluded_entities = EntityHashSet::from_iter(entities); self } - /// Tests if an entity should be included in [spatial queries](crate::spatial_query) based on the - /// filter configuration. + /// Tests if an entity should be included in [spatial queries] based on the filter configuration. + /// + /// [spatial queries]: crate::spatial_query pub fn test(&self, entity: Entity, layers: CollisionLayers) -> bool { !self.excluded_entities.contains(&entity) && CollisionLayers::new(LayerMask::ALL, self.mask) diff --git a/src/spatial_query/ray_caster.rs b/src/spatial_query/ray_caster.rs index 6b5ea9ee..f7d59c1b 100644 --- a/src/spatial_query/ray_caster.rs +++ b/src/spatial_query/ray_caster.rs @@ -21,8 +21,8 @@ use parry::query::{ /// between a ray and a set of colliders. /// /// Each ray is defined by a local `origin` and a `direction`. The [`RayCaster`] will find each hit -/// and add them to the [`RayHits`] component. Each hit has a `time_of_impact` property -/// which refers to how long the ray travelled, i.e. the distance between the `origin` and the point of intersection. +/// and add them to the [`RayHits`] component. Each hit has a `distance` property which refers to +/// how far the ray travelled, along with a `normal` for the point of intersection. /// /// The [`RayCaster`] is the easiest way to handle simple raycasts. If you want more control and don't want to /// perform raycasts every frame, consider using the [`SpatialQuery`] system parameter. @@ -30,7 +30,7 @@ use parry::query::{ /// # Hit Count and Order /// /// The results of a raycast are in an arbitrary order by default. You can iterate over them in the order of -/// time of impact with the [`RayHits::iter_sorted`] method. +/// distance with the [`RayHits::iter_sorted`] method. /// /// You can configure the maximum amount of hits for a ray using `max_hits`. By default this is unbounded, /// so you will get all hits. When the number or complexity of colliders is large, this can be very @@ -64,7 +64,7 @@ use parry::query::{ /// println!( /// "Hit entity {} at {} with normal {}", /// hit.entity, -/// ray.origin + *ray.direction * hit.time_of_impact, +/// ray.origin + *ray.direction * hit.distance, /// hit.normal, /// ); /// } @@ -80,36 +80,47 @@ use parry::query::{ pub struct RayCaster { /// Controls if the ray caster is enabled. pub enabled: bool, + /// The local origin of the ray relative to the [`Position`] and [`Rotation`] of the ray entity or its parent. /// /// To get the global origin, use the `global_origin` method. pub origin: Vector, + /// The global origin of the ray. global_origin: Vector, + /// The local direction of the ray relative to the [`Rotation`] of the ray entity or its parent. /// /// To get the global direction, use the `global_direction` method. pub direction: Dir, + /// The global direction of the ray. global_direction: Dir, - /// The maximum distance the ray can travel. By default this is infinite, so the ray will travel - /// until all hits up to `max_hits` have been checked. - pub max_time_of_impact: Scalar, + /// The maximum number of hits allowed. /// /// When there are more hits than `max_hits`, **some hits will be missed**. /// To guarantee that the closest hit is included, you should set `max_hits` to one or a value that /// is enough to contain all hits. pub max_hits: u32, + + /// The maximum distance the ray can travel. + /// + /// By default this is infinite, so the ray will travel until all hits up to `max_hits` have been checked. + #[doc(alias = "max_time_of_impact")] + pub max_distance: Scalar, + /// Controls how the ray behaves when the ray origin is inside of a [collider](Collider). /// - /// If `solid` is true, the point of intersection will be the ray origin itself.\ - /// If `solid` is false, the collider will be considered to have no interior, and the point of intersection - /// will be at the collider shape's boundary. + /// If `true`, shapes will be treated as solid, and the ray cast will return with a distance of `0.0` + /// if the ray origin is inside of the shape. Otherwise, shapes will be treated as hollow, and the ray + /// will always return a hit at the shape's boundary. pub solid: bool, + /// If true, the ray caster ignores hits against its own [`Collider`]. This is the default. pub ignore_self: bool, - /// Rules that determine which colliders are taken into account in the query. + + /// Rules that determine which colliders are taken into account in the ray cast. pub query_filter: SpatialQueryFilter, } @@ -121,7 +132,7 @@ impl Default for RayCaster { global_origin: Vector::ZERO, direction: Dir::X, global_direction: Dir::X, - max_time_of_impact: Scalar::MAX, + max_distance: Scalar::MAX, max_hits: u32::MAX, solid: true, ignore_self: true, @@ -167,29 +178,36 @@ impl RayCaster { self } - /// Sets if the ray treats [colliders](Collider) as solid. + /// Controls how the ray behaves when the ray origin is inside of a [collider](Collider). /// - /// If `solid` is true, the point of intersection will be the ray origin itself.\ - /// If `solid` is false, the collider will be considered to have no interior, and the point of intersection - /// will be at the collider shape's boundary. + /// If `true`, shapes will be treated as solid, and the ray cast will return with a distance of `0.0` + /// if the ray origin is inside of the shape. Otherwise, shapes will be treated as hollow, and the ray + /// will always return a hit at the shape's boundary. pub fn with_solidness(mut self, solid: bool) -> Self { self.solid = solid; self } /// Sets if the ray caster should ignore hits against its own [`Collider`]. - /// The default is true. + /// + /// The default is `true`. pub fn with_ignore_self(mut self, ignore: bool) -> Self { self.ignore_self = ignore; self } - /// Sets the maximum time of impact, i.e. the maximum distance that the ray is allowed to travel. - pub fn with_max_time_of_impact(mut self, max_time_of_impact: Scalar) -> Self { - self.max_time_of_impact = max_time_of_impact; + /// Sets the maximum distance the ray can travel. + pub fn with_max_distance(mut self, max_distance: Scalar) -> Self { + self.max_distance = max_distance; self } + /// Sets the maximum time of impact, i.e. the maximum distance that the ray is allowed to travel. + #[deprecated(since = "0.2.0", note = "Renamed to `with_max_distance`")] + pub fn with_max_time_of_impact(self, max_time_of_impact: Scalar) -> Self { + self.with_max_distance(max_time_of_impact) + } + /// Sets the maximum number of allowed hits. pub fn with_max_hits(mut self, max_hits: u32) -> Self { self.max_hits = max_hits; @@ -260,14 +278,14 @@ impl RayCaster { let mut visitor = RayCompositeShapeToiAndNormalBestFirstVisitor::new( &pipeline_shape, &ray, - self.max_time_of_impact, + self.max_distance, self.solid, ); if let Some(hit) = query_pipeline.qbvh.traverse_best_first(&mut visitor).map( |(_, (entity_index, hit))| RayHitData { entity: query_pipeline.entity_from_index(entity_index), - time_of_impact: hit.time_of_impact, + distance: hit.time_of_impact, normal: hit.normal.into(), }, ) { @@ -291,19 +309,19 @@ impl RayCaster { if let Some(hit) = shape.shape_scaled().cast_ray_and_get_normal( iso, &ray, - self.max_time_of_impact, + self.max_distance, self.solid, ) { if (hits.vector.len() as u32) < hits.count + 1 { hits.vector.push(RayHitData { entity, - time_of_impact: hit.time_of_impact, + distance: hit.time_of_impact, normal: hit.normal.into(), }); } else { hits.vector[hits.count as usize] = RayHitData { entity, - time_of_impact: hit.time_of_impact, + distance: hit.time_of_impact, normal: hit.normal.into(), }; } @@ -318,7 +336,7 @@ impl RayCaster { }; let mut visitor = - RayIntersectionsVisitor::new(&ray, self.max_time_of_impact, &mut leaf_callback); + RayIntersectionsVisitor::new(&ray, self.max_distance, &mut leaf_callback); query_pipeline.qbvh.traverse_depth_first(&mut visitor); } } @@ -344,7 +362,7 @@ fn on_add_ray_caster(mut world: DeferredWorld, entity: Entity, _component_id: Co /// /// By default, the order of the hits is not guaranteed. /// -/// You can iterate the hits in the order of time of impact with `iter_sorted`. +/// You can iterate the hits in the order of distance with `iter_sorted`. /// Note that this will create and sort a new vector instead of the original one. /// /// **Note**: When there are more hits than `max_hits`, **some hits @@ -363,11 +381,7 @@ fn on_add_ray_caster(mut world: DeferredWorld, entity: Entity, _component_id: Co /// for hits in &query { /// // For the faster iterator that isn't sorted, use `.iter()` /// for hit in hits.iter_sorted() { -/// println!( -/// "Hit entity {} with time of impact {}", -/// hit.entity, -/// hit.time_of_impact, -/// ); +/// println!("Hit entity {} with distance {}", hit.entity, hit.distance); /// } /// } /// } @@ -407,17 +421,17 @@ impl RayHits { /// Returns an iterator over the hits in arbitrary order. /// - /// If you want to get them sorted by time of impact, use `iter_sorted`. + /// If you want to get them sorted by distance, use `iter_sorted`. pub fn iter(&self) -> std::slice::Iter { self.as_slice().iter() } - /// Returns an iterator over the hits, sorted in ascending order according to the time of impact. + /// Returns an iterator over the hits, sorted in ascending order according to the distance. /// /// Note that this creates and sorts a new vector. If you don't need the hits in order, use `iter`. pub fn iter_sorted(&self) -> std::vec::IntoIter { let mut vector = self.as_slice().to_vec(); - vector.sort_by(|a, b| a.time_of_impact.partial_cmp(&b.time_of_impact).unwrap()); + vector.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap()); vector.into_iter() } } @@ -438,9 +452,11 @@ impl MapEntities for RayHits { pub struct RayHitData { /// The entity of the collider that was hit by the ray. pub entity: Entity, - /// How long the ray travelled, i.e. the distance between the ray origin and the point of intersection. - pub time_of_impact: Scalar, - /// The normal at the point of intersection. + + /// How far the ray travelled. This is the distance between the ray origin and the point of intersection. + pub distance: Scalar, + + /// The normal at the point of intersection, expressed in world space. pub normal: Vector, } diff --git a/src/spatial_query/shape_caster.rs b/src/spatial_query/shape_caster.rs index bd14ace6..68ac0000 100644 --- a/src/spatial_query/shape_caster.rs +++ b/src/spatial_query/shape_caster.rs @@ -17,7 +17,7 @@ use parry::query::{details::TOICompositeShapeShapeBestFirstVisitor, ShapeCastOpt /// /// Each shapecast is defined by a `shape` (a [`Collider`]), its local `shape_rotation`, a local `origin` and /// a local `direction`. The [`ShapeCaster`] will find each hit and add them to the [`ShapeHits`] component in -/// the order of the time of impact. +/// the order of distance. /// /// Computing lots of hits can be expensive, especially against complex geometry, so the maximum number of hits /// is one by default. This can be configured through the `max_hits` property. @@ -63,55 +63,83 @@ use parry::query::{details::TOICompositeShapeShapeBestFirstVisitor, ShapeCastOpt pub struct ShapeCaster { /// Controls if the shape caster is enabled. pub enabled: bool, + /// The shape being cast represented as a [`Collider`]. #[reflect(ignore)] pub shape: Collider, + /// The local origin of the shape relative to the [`Position`] and [`Rotation`] /// of the shape caster entity or its parent. /// /// To get the global origin, use the `global_origin` method. pub origin: Vector, + /// The global origin of the shape. global_origin: Vector, + /// The local rotation of the shape being cast relative to the [`Rotation`] /// of the shape caster entity or its parent. Expressed in radians. /// /// To get the global shape rotation, use the `global_shape_rotation` method. #[cfg(feature = "2d")] pub shape_rotation: Scalar, + /// The local rotation of the shape being cast relative to the [`Rotation`] /// of the shape caster entity or its parent. /// /// To get the global shape rotation, use the `global_shape_rotation` method. #[cfg(feature = "3d")] pub shape_rotation: Quaternion, + /// The global rotation of the shape. #[cfg(feature = "2d")] global_shape_rotation: Scalar, + /// The global rotation of the shape. #[cfg(feature = "3d")] global_shape_rotation: Quaternion, + /// The local direction of the shapecast relative to the [`Rotation`] of the shape caster entity or its parent. /// /// To get the global direction, use the `global_direction` method. pub direction: Dir, + /// The global direction of the shapecast. global_direction: Dir, - /// The maximum distance the shape can travel. By default this is infinite, so the shape will travel - /// until a hit is found. - pub max_time_of_impact: Scalar, + /// The maximum number of hits allowed. By default this is one and only the first hit is returned. pub max_hits: u32, - /// Controls how the shapecast behaves when the shape is already penetrating a [collider](Collider) - /// at the shape origin. + + /// The maximum distance the shape can travel. /// - /// If set to true **and** the shape is being cast in a direction where it will eventually stop penetrating, - /// the shapecast will not stop immediately, and will instead continue until another hit.\ - /// If set to false, the shapecast will stop immediately and return the hit. This is the default. + /// By default, this is infinite. + #[doc(alias = "max_time_of_impact")] + pub max_distance: Scalar, + + /// The separation distance at which the shapes will be considered as impacting. + /// + /// If the shapes are separated by a distance smaller than `target_distance` at the origin of the cast, + /// the computed contact points and normals are only reliable if [`ShapeCaster::compute_contact_on_penetration`] + /// is set to `true`. + /// + /// By default, this is `0.0`, so the shapes will only be considered as impacting when they first touch. + pub target_distance: Scalar, + + /// If `true`, contact points and normals will be calculated even when the cast distance is `0.0`. + /// + /// The default is `true`. + pub compute_contact_on_penetration: bool, + + /// If `true` *and* the shape is travelling away from the object that was hit, + /// the cast will ignore any impact that happens at the cast origin. + /// + /// The default is `false`. pub ignore_origin_penetration: bool, + /// If true, the shape caster ignores hits against its own [`Collider`]. This is the default. pub ignore_self: bool, - /// Rules that determine which colliders are taken into account in the query. + + /// Rules that determine which colliders are taken into account in the shape cast. pub query_filter: SpatialQueryFilter, } @@ -135,8 +163,10 @@ impl Default for ShapeCaster { global_shape_rotation: Quaternion::IDENTITY, direction: Dir::X, global_direction: Dir::X, - max_time_of_impact: Scalar::MAX, max_hits: 1, + max_distance: Scalar::MAX, + target_distance: 0.0, + compute_contact_on_penetration: true, ignore_origin_penetration: false, ignore_self: true, query_filter: SpatialQueryFilter::default(), @@ -190,10 +220,30 @@ impl ShapeCaster { self } + /// Sets the separation distance at which the shapes will be considered as impacting. + /// + /// If the shapes are separated by a distance smaller than `target_distance` at the origin of the cast, + /// the computed contact points and normals are only reliable if [`ShapeCaster::compute_contact_on_penetration`] + /// is set to `true`. + /// + /// By default, this is `0.0`, so the shapes will only be considered as impacting when they first touch. + pub fn with_target_distance(mut self, target_distance: Scalar) -> Self { + self.target_distance = target_distance; + self + } + + /// Sets if contact points and normals should be calculated even when the cast distance is `0.0`. + /// + /// The default is `true`. + pub fn with_compute_contact_on_penetration(mut self, compute_contact: bool) -> Self { + self.compute_contact_on_penetration = compute_contact; + self + } + /// Controls how the shapecast behaves when the shape is already penetrating a [collider](Collider) /// at the shape origin. /// - /// If set to true **and** the shape is being cast in a direction where it will eventually stop penetrating, + /// If set to `true` **and** the shape is being cast in a direction where it will eventually stop penetrating, /// the shapecast will not stop immediately, and will instead continue until another hit.\ /// If set to false, the shapecast will stop immediately and return the hit. This is the default. pub fn with_ignore_origin_penetration(mut self, ignore: bool) -> Self { @@ -202,18 +252,25 @@ impl ShapeCaster { } /// Sets if the shape caster should ignore hits against its own [`Collider`]. - /// The default is true. + /// + /// The default is `true`. pub fn with_ignore_self(mut self, ignore: bool) -> Self { self.ignore_self = ignore; self } - /// Sets the maximum time of impact, i.e. the maximum distance that the ray is allowed to travel. - pub fn with_max_time_of_impact(mut self, max_time_of_impact: Scalar) -> Self { - self.max_time_of_impact = max_time_of_impact; + /// Sets the maximum distance the shape can travel. + pub fn with_max_distance(mut self, max_distance: Scalar) -> Self { + self.max_distance = max_distance; self } + /// Sets the maximum time of impact, i.e. the maximum distance that the shape is allowed to travel. + #[deprecated(since = "0.2.0", note = "Renamed to `with_max_distance`")] + pub fn with_max_time_of_impact(self, max_time_of_impact: Scalar) -> Self { + self.with_max_distance(max_time_of_impact) + } + /// Sets the maximum number of allowed hits. pub fn with_max_hits(mut self, max_hits: u32) -> Self { self.max_hits = max_hits; @@ -320,7 +377,7 @@ impl ShapeCaster { &pipeline_shape, &**self.shape.shape_scaled(), ShapeCastOptions { - max_time_of_impact: self.max_time_of_impact, + max_time_of_impact: self.max_distance, stop_at_penetration: !self.ignore_origin_penetration, ..default() }, @@ -329,7 +386,7 @@ impl ShapeCaster { if let Some(hit) = query_pipeline.qbvh.traverse_best_first(&mut visitor).map( |(_, (entity_index, hit))| ShapeHitData { entity: query_pipeline.entity_from_index(entity_index), - time_of_impact: hit.time_of_impact, + distance: hit.time_of_impact, point1: hit.witness1.into(), point2: hit.witness2.into(), normal1: hit.normal1.into(), @@ -363,7 +420,93 @@ fn on_add_shape_caster(mut world: DeferredWorld, entity: Entity, _component_id: world.get_mut::(entity).unwrap().vector = Vec::with_capacity(max_hits); } -/// Contains the hits of a shape cast by a [`ShapeCaster`]. The hits are in the order of time of impact. +/// Configuration for a shape cast. +#[derive(Clone, Debug, PartialEq, Reflect)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] +#[reflect(Debug, PartialEq)] +pub struct ShapeCastConfig { + /// The maximum distance the shape can travel. + /// + /// By default, this is infinite. + #[doc(alias = "max_time_of_impact")] + pub max_distance: Scalar, + + /// The separation distance at which the shapes will be considered as impacting. + /// + /// If the shapes are separated by a distance smaller than `target_distance` at the origin of the cast, + /// the computed contact points and normals are only reliable if [`ShapeCastConfig::compute_contact_on_penetration`] + /// is set to `true`. + /// + /// By default, this is `0.0`, so the shapes will only be considered as impacting when they first touch. + pub target_distance: Scalar, + + /// If `true`, contact points and normals will be calculated even when the cast distance is `0.0`. + /// + /// The default is `true`. + pub compute_contact_on_penetration: bool, + + /// If `true` *and* the shape is travelling away from the object that was hit, + /// the cast will ignore any impact that happens at the cast origin. + /// + /// The default is `false`. + pub ignore_origin_penetration: bool, +} + +impl Default for ShapeCastConfig { + fn default() -> Self { + Self::DEFAULT + } +} + +impl ShapeCastConfig { + /// The default [`ShapeCastConfig`] configuration. + pub const DEFAULT: Self = Self { + max_distance: Scalar::MAX, + target_distance: 0.0, + compute_contact_on_penetration: true, + ignore_origin_penetration: false, + }; + + /// Creates a new [`ShapeCastConfig`] with a given maximum distance the shape can travel. + #[inline] + pub const fn from_max_distance(max_distance: Scalar) -> Self { + Self { + max_distance, + target_distance: 0.0, + compute_contact_on_penetration: true, + ignore_origin_penetration: false, + } + } + + /// Creates a new [`ShapeCastConfig`] with a given separation distance at which + /// the shapes will be considered as impacting. + #[inline] + pub const fn from_target_distance(target_distance: Scalar) -> Self { + Self { + max_distance: Scalar::MAX, + target_distance, + compute_contact_on_penetration: true, + ignore_origin_penetration: false, + } + } + + /// Sets the maximum distance the shape can travel. + #[inline] + pub const fn with_max_distance(mut self, max_distance: Scalar) -> Self { + self.max_distance = max_distance; + self + } + + /// Sets the separation distance at which the shapes will be considered as impacting. + #[inline] + pub const fn with_target_distance(mut self, target_distance: Scalar) -> Self { + self.target_distance = target_distance; + self + } +} + +/// Contains the hits of a shape cast by a [`ShapeCaster`]. The hits are in the order of distance. /// /// The maximum number of hits depends on the value of `max_hits` in [`ShapeCaster`]. By default only /// one hit is computed, as shapecasting for many results can be expensive. @@ -380,11 +523,7 @@ fn on_add_shape_caster(mut world: DeferredWorld, entity: Entity, _component_id: /// fn print_hits(query: Query<&ShapeHits, With>) { /// for hits in &query { /// for hit in hits.iter() { -/// println!( -/// "Hit entity {} with time of impact {}", -/// hit.entity, -/// hit.time_of_impact, -/// ); +/// println!("Hit entity {} with distance {}", hit.entity, hit.distance); /// } /// } /// } @@ -421,7 +560,7 @@ impl ShapeHits { self.count = 0; } - /// Returns an iterator over the hits in the order of time of impact. + /// Returns an iterator over the hits in the order of distance. pub fn iter(&self) -> std::slice::Iter { self.as_slice().iter() } @@ -443,19 +582,27 @@ impl MapEntities for ShapeHits { pub struct ShapeHitData { /// The entity of the collider that was hit by the shape. pub entity: Entity, - /// The time of impact (TOI), or how long the shape travelled before the initial hit. - pub time_of_impact: Scalar, - /// The closest point on the collider that was hit by the shapecast, at the time of impact, - /// expressed in the local space of the collider shape. + + /// How far the shape travelled before the initial hit. + #[doc(alias = "time_of_impact")] + pub distance: Scalar, + + /// The closest point on the shape that was hit, expressed in world space. + /// + /// If the shapes are penetrating or the target distance is greater than zero, + /// this will be different from `point2`. pub point1: Vector, - /// The closest point on the cast shape, at the time of impact, - /// expressed in the local space of the cast shape. + + /// The closest point on the shape that was cast, expressed in world space. + /// + /// If the shapes are penetrating or the target distance is greater than zero, + /// this will be different from `point1`. pub point2: Vector, - /// The outward normal on the collider that was hit by the shapecast, at the time of impact, - /// expressed in the local space of the collider shape. + + /// The outward surface normal on the hit shape at `point1`, expressed in world space. pub normal1: Vector, - /// The outward normal on the cast shape, at the time of impact, - /// expressed in the local space of the cast shape. + + /// The outward surface normal on the cast shape at `point2`, expressed in world space. pub normal2: Vector, } diff --git a/src/spatial_query/system_param.rs b/src/spatial_query/system_param.rs index 4075ecd1..bc057b22 100644 --- a/src/spatial_query/system_param.rs +++ b/src/spatial_query/system_param.rs @@ -5,11 +5,11 @@ use bevy::{ecs::system::SystemParam, prelude::*}; /// /// # Methods /// -/// - [Raycasting](spatial_query#raycasting): [`cast_ray`](SpatialQuery::cast_ray), +/// - [Raycasting](spatial_query#raycasting): [`cast_ray`](SpatialQuery::cast_ray), [`cast_ray_predicate`](SpatialQuery::cast_ray_predicate), /// [`ray_hits`](SpatialQuery::ray_hits), [`ray_hits_callback`](SpatialQuery::ray_hits_callback) -/// - [Shapecasting](spatial_query#shapecasting): [`cast_shape`](SpatialQuery::cast_shape), +/// - [Shapecasting](spatial_query#shapecasting): [`cast_shape`](SpatialQuery::cast_shape), [`cast_shape_predicate`](SpatialQuery::cast_shape_predicate), /// [`shape_hits`](SpatialQuery::shape_hits), [`shape_hits_callback`](SpatialQuery::shape_hits_callback) -/// - [Point projection](spatial_query#point-projection): [`project_point`](SpatialQuery::project_point) +/// - [Point projection](spatial_query#point-projection): [`project_point`](SpatialQuery::project_point) and [`project_point_predicate`](SpatialQuery::project_point_predicate) /// - [Intersection tests](spatial_query#intersection-tests) /// - Point intersections: [`point_intersections`](SpatialQuery::point_intersections), /// [`point_intersections_callback`](SpatialQuery::point_intersections_callback) @@ -32,26 +32,22 @@ use bevy::{ecs::system::SystemParam, prelude::*}; /// /// # #[cfg(all(feature = "3d", feature = "f32"))] /// fn print_hits(spatial_query: SpatialQuery) { +/// // Ray origin and direction +/// let origin = Vec3::ZERO; +/// let direction = Dir3::X; +/// +/// // Configuration for the ray cast +/// let max_distance = 100.0; +/// let solid = true; +/// let filter = SpatialQueryFilter::default(); +/// /// // Cast ray and print first hit -/// if let Some(first_hit) = spatial_query.cast_ray( -/// Vec3::ZERO, // Origin -/// Dir3::X, // Direction -/// 100.0, // Maximum time of impact (travel distance) -/// true, // Does the ray treat colliders as "solid" -/// &SpatialQueryFilter::default(),// Query filter -/// ) { +/// if let Some(first_hit) = spatial_query.cast_ray(origin, direction, max_distance, solid, &filter) { /// println!("First hit: {:?}", first_hit); /// } /// /// // Cast ray and get up to 20 hits -/// let hits = spatial_query.ray_hits( -/// Vec3::ZERO, // Origin -/// Dir3::X, // Direction -/// 100.0, // Maximum time of impact (travel distance) -/// 20, // Maximum number of hits -/// true, // Does the ray treat colliders as "solid" -/// &SpatialQueryFilter::default(),// Query filter -/// ); +/// let hits = spatial_query.ray_hits(origin, direction, max_distance, 20, solid, &filter); /// /// // Print hits /// for hit in hits.iter() { @@ -93,10 +89,10 @@ impl SpatialQuery<'_, '_> { /// /// - `origin`: Where the ray is cast from. /// - `direction`: What direction the ray is cast in. - /// - `max_time_of_impact`: The maximum distance that the ray can travel. - /// - `solid`: If true and the ray origin is inside of a collider, the hit point will be the ray origin itself. - /// Otherwise, the collider will be treated as hollow, and the hit point will be at the collider's boundary. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `max_distance`: The maximum distance the ray can travel. + /// - `solid`: If true *and* the ray origin is inside of a collider, the hit point will be the ray origin itself. + /// Otherwise, the collider will be treated as hollow, and the hit point will be at its boundary. + /// - `filter`: A [`SpatialQueryFilter`] that determines which entities are included in the cast. /// /// # Example /// @@ -109,28 +105,37 @@ impl SpatialQuery<'_, '_> { /// /// # #[cfg(all(feature = "3d", feature = "f32"))] /// fn print_hits(spatial_query: SpatialQuery) { + /// // Ray origin and direction + /// let origin = Vec3::ZERO; + /// let direction = Dir3::X; + /// + /// // Configuration for the ray cast + /// let max_distance = 100.0; + /// let solid = true; + /// let filter = SpatialQueryFilter::default(); + /// /// // Cast ray and print first hit - /// if let Some(first_hit) = spatial_query.cast_ray( - /// Vec3::ZERO, // Origin - /// Dir3::X, // Direction - /// 100.0, // Maximum time of impact (travel distance) - /// true, // Does the ray treat colliders as "solid" - /// &SpatialQueryFilter::default(),// Query filter - /// ) { + /// if let Some(first_hit) = spatial_query.cast_ray(origin, direction, max_distance, solid, &filter) { /// println!("First hit: {:?}", first_hit); /// } /// } /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::cast_ray_predicate`] + /// - [`SpatialQuery::ray_hits`] + /// - [`SpatialQuery::ray_hits_callback`] pub fn cast_ray( &self, origin: Vector, direction: Dir, - max_time_of_impact: Scalar, + max_distance: Scalar, solid: bool, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, ) -> Option { self.query_pipeline - .cast_ray(origin, direction, max_time_of_impact, solid, query_filter) + .cast_ray(origin, direction, max_distance, solid, filter) } /// Casts a [ray](spatial_query#raycasting) and computes the closest [hit](RayHitData) with a collider. @@ -140,12 +145,11 @@ impl SpatialQuery<'_, '_> { /// /// - `origin`: Where the ray is cast from. /// - `direction`: What direction the ray is cast in. - /// - `max_time_of_impact`: The maximum distance that the ray can travel. - /// - `solid`: If true and the ray origin is inside of a collider, the hit point will be the ray origin itself. - /// Otherwise, the collider will be treated as hollow, and the hit point will be at the collider's boundary. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. - /// - `predicate`: A function with which the colliders are filtered. Given the Entity it should return false, if the - /// entity should be ignored. + /// - `max_distance`: The maximum distance the ray can travel. + /// - `solid`: If true *and* the ray origin is inside of a collider, the hit point will be the ray origin itself. + /// Otherwise, the collider will be treated as hollow, and the hit point will be at its boundary. + /// - `filter`: A [`SpatialQueryFilter`] that determines which entities are included in the cast. + /// - `predicate`: A function called on each entity hit by the ray. The ray keeps travelling until the predicate returns `false`. /// /// # Example /// @@ -161,37 +165,48 @@ impl SpatialQuery<'_, '_> { /// /// # #[cfg(all(feature = "3d", feature = "f32"))] /// fn print_hits(spatial_query: SpatialQuery, query: Query<&Invisible>) { - /// // Cast ray and print first hit - /// if let Some(first_hit) = spatial_query.cast_ray_predicate( - /// Vec3::ZERO, // Origin - /// Dir3::X, // Direction - /// 100.0, // Maximum time of impact (travel distance) - /// true, // Does the ray treat colliders as "solid" - /// &SpatialQueryFilter::default(),// Query filter - /// &|entity| { // Predicate - /// // Skip entities with the `Invisible` component. - /// !query.contains(entity) - /// } - /// ) { + /// // Ray origin and direction + /// let origin = Vec3::ZERO; + /// let direction = Dir3::X; + /// + /// // Configuration for the ray cast + /// let max_distance = 100.0; + /// let solid = true; + /// let filter = SpatialQueryFilter::default(); + /// + /// // Cast ray and get the first hit that matches the predicate + /// let hit = spatial_query.cast_ray_predicate(origin, direction, max_distance, solid, &filter, &|entity| { + /// // Skip entities with the `Invisible` component. + /// !query.contains(entity) + /// }); + /// + /// // Print first hit + /// if let Some(first_hit) = hit { /// println!("First hit: {:?}", first_hit); /// } /// } /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::cast_ray`] + /// - [`SpatialQuery::ray_hits`] + /// - [`SpatialQuery::ray_hits_callback`] pub fn cast_ray_predicate( &self, origin: Vector, direction: Dir, - max_time_of_impact: Scalar, + max_distance: Scalar, solid: bool, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, predicate: &dyn Fn(Entity) -> bool, ) -> Option { self.query_pipeline.cast_ray_predicate( origin, direction, - max_time_of_impact, + max_distance, solid, - query_filter, + filter, predicate, ) } @@ -205,11 +220,11 @@ impl SpatialQuery<'_, '_> { /// /// - `origin`: Where the ray is cast from. /// - `direction`: What direction the ray is cast in. - /// - `max_time_of_impact`: The maximum distance that the ray can travel. + /// - `max_distance`: The maximum distance the ray can travel. /// - `max_hits`: The maximum number of hits. Additional hits will be missed. - /// - `solid`: If true and the ray origin is inside of a collider, the hit point will be the ray origin itself. - /// Otherwise, the collider will be treated as hollow, and the hit point will be at the collider's boundary. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `solid`: If true *and* the ray origin is inside of a collider, the hit point will be the ray origin itself. + /// Otherwise, the collider will be treated as hollow, and the hit point will be at its boundary. + /// - `filter`: A [`SpatialQueryFilter`] that determines which entities are included in the cast. /// /// # Example /// @@ -222,15 +237,17 @@ impl SpatialQuery<'_, '_> { /// /// # #[cfg(all(feature = "3d", feature = "f32"))] /// fn print_hits(spatial_query: SpatialQuery) { - /// // Cast ray and get hits - /// let hits = spatial_query.ray_hits( - /// Vec3::ZERO, // Origin - /// Dir3::X, // Direction - /// 100.0, // Maximum time of impact (travel distance) - /// 20, // Maximum number of hits - /// true, // Does the ray treat colliders as "solid" - /// &SpatialQueryFilter::default(),// Query filter - /// ); + /// // Ray origin and direction + /// let origin = Vec3::ZERO; + /// let direction = Dir3::X; + /// + /// // Configuration for the ray cast + /// let max_distance = 100.0; + /// let solid = true; + /// let filter = SpatialQueryFilter::default(); + /// + /// // Cast ray and get up to 20 hits + /// let hits = spatial_query.ray_hits(origin, direction, max_distance, 20, solid, &filter); /// /// // Print hits /// for hit in hits.iter() { @@ -238,23 +255,23 @@ impl SpatialQuery<'_, '_> { /// } /// } /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::cast_ray`] + /// - [`SpatialQuery::cast_ray_predicate`] + /// - [`SpatialQuery::ray_hits_callback`] pub fn ray_hits( &self, origin: Vector, direction: Dir, - max_time_of_impact: Scalar, + max_distance: Scalar, max_hits: u32, solid: bool, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, ) -> Vec { - self.query_pipeline.ray_hits( - origin, - direction, - max_time_of_impact, - max_hits, - solid, - query_filter, - ) + self.query_pipeline + .ray_hits(origin, direction, max_distance, max_hits, solid, filter) } /// Casts a [ray](spatial_query#raycasting) and computes all [hits](RayHitData), calling the given `callback` @@ -266,10 +283,10 @@ impl SpatialQuery<'_, '_> { /// /// - `origin`: Where the ray is cast from. /// - `direction`: What direction the ray is cast in. - /// - `max_time_of_impact`: The maximum distance that the ray can travel. - /// - `solid`: If true and the ray origin is inside of a collider, the hit point will be the ray origin itself. - /// Otherwise, the collider will be treated as hollow, and the hit point will be at the collider's boundary. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `max_distance`: The maximum distance the ray can travel. + /// - `solid`: If true *and* the ray origin is inside of a collider, the hit point will be the ray origin itself. + /// Otherwise, the collider will be treated as hollow, and the hit point will be at its boundary. + /// - `filter`: A [`SpatialQueryFilter`] that determines which entities are included in the cast. /// - `callback`: A callback function called for each hit. /// /// # Example @@ -283,20 +300,21 @@ impl SpatialQuery<'_, '_> { /// /// # #[cfg(all(feature = "3d", feature = "f32"))] /// fn print_hits(spatial_query: SpatialQuery) { - /// let mut hits = vec![]; + /// // Ray origin and direction + /// let origin = Vec3::ZERO; + /// let direction = Dir3::X; + /// + /// // Configuration for the ray cast + /// let max_distance = 100.0; + /// let solid = true; + /// let filter = SpatialQueryFilter::default(); /// /// // Cast ray and get all hits - /// spatial_query.ray_hits_callback( - /// Vec3::ZERO, // Origin - /// Dir3::X, // Direction - /// 100.0, // Maximum time of impact (travel distance) - /// true, // Does the ray treat colliders as "solid" - /// &SpatialQueryFilter::default(),// Query filter - /// |hit| { // Callback function - /// hits.push(hit); - /// true - /// }, - /// ); + /// let mut hits = vec![]; + /// spatial_query.ray_hits_callback(origin, direction, max_distance, 20, solid, &filter, |hit| { + /// hits.push(hit); + /// true + /// }); /// /// // Print hits /// for hit in hits.iter() { @@ -304,26 +322,32 @@ impl SpatialQuery<'_, '_> { /// } /// } /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::cast_ray`] + /// - [`SpatialQuery::cast_ray_predicate`] + /// - [`SpatialQuery::ray_hits`] pub fn ray_hits_callback( &self, origin: Vector, direction: Dir, - max_time_of_impact: Scalar, + max_distance: Scalar, solid: bool, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, callback: impl FnMut(RayHitData) -> bool, ) { self.query_pipeline.ray_hits_callback( origin, direction, - max_time_of_impact, + max_distance, solid, - query_filter, + filter, callback, ) } - /// Casts a [shape](spatial_query#shapecasting) with a given rotation and computes the closest [hit](ShapeHits) + /// Casts a [shape](spatial_query#shapecasting) with a given rotation and computes the closest [hit](ShapeHitData) /// with a collider. If there are no hits, `None` is returned. /// /// For a more ECS-based approach, consider using the [`ShapeCaster`] component instead. @@ -334,11 +358,8 @@ impl SpatialQuery<'_, '_> { /// - `origin`: Where the shape is cast from. /// - `shape_rotation`: The rotation of the shape being cast. /// - `direction`: What direction the shape is cast in. - /// - `max_time_of_impact`: The maximum distance that the shape can travel. - /// - `ignore_origin_penetration`: If true and the shape is already penetrating a collider at the - /// shape origin, the hit will be ignored and only the next hit will be computed. Otherwise, the initial - /// hit will be returned. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `config`: A [`ShapeCastConfig`] that determines the behavior of the cast. + /// - `filter`: A [`SpatialQueryFilter`] that determines which entities are included in the cast. /// /// # Example /// @@ -351,20 +372,29 @@ impl SpatialQuery<'_, '_> { /// /// # #[cfg(all(feature = "3d", feature = "f32"))] /// fn print_hits(spatial_query: SpatialQuery) { - /// // Cast ray and print first hit - /// if let Some(first_hit) = spatial_query.cast_shape( - /// &Collider::sphere(0.5), // Shape - /// Vec3::ZERO, // Origin - /// Quat::default(), // Shape rotation - /// Dir3::X, // Direction - /// 100.0, // Maximum time of impact (travel distance) - /// true, // Should initial penetration at the origin be ignored - /// &SpatialQueryFilter::default(), // Query filter - /// ) { + /// // Shape properties + /// let shape = Collider::sphere(0.5); + /// let origin = Vec3::ZERO; + /// let rotation = Quat::default(); + /// let direction = Dir3::X; + /// + /// // Configuration for the shape cast + /// let config = ShapeCastConfig::from_max_distance(100.0); + /// let filter = SpatialQueryFilter::default(); + /// + /// // Cast shape and print first hit + /// if let Some(first_hit) = spatial_query.cast_shape(&shape, origin, rotation, direction, &config, &filter) + /// { /// println!("First hit: {:?}", first_hit); /// } /// } /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::cast_shape_predicate`] + /// - [`SpatialQuery::shape_hits`] + /// - [`SpatialQuery::shape_hits_callback`] #[allow(clippy::too_many_arguments)] pub fn cast_shape( &self, @@ -372,23 +402,93 @@ impl SpatialQuery<'_, '_> { origin: Vector, shape_rotation: RotationValue, direction: Dir, - max_time_of_impact: Scalar, - ignore_origin_penetration: bool, - query_filter: &SpatialQueryFilter, + config: &ShapeCastConfig, + filter: &SpatialQueryFilter, ) -> Option { - self.query_pipeline.cast_shape( + self.query_pipeline + .cast_shape(shape, origin, shape_rotation, direction, config, filter) + } + + /// Casts a [shape](spatial_query#shapecasting) with a given rotation and computes the closest [hit](ShapeHitData) + /// with a collider. If there are no hits, `None` is returned. + /// + /// For a more ECS-based approach, consider using the [`ShapeCaster`] component instead. + /// + /// # Arguments + /// + /// - `shape`: The shape being cast represented as a [`Collider`]. + /// - `origin`: Where the shape is cast from. + /// - `shape_rotation`: The rotation of the shape being cast. + /// - `direction`: What direction the shape is cast in. + /// - `config`: A [`ShapeCastConfig`] that determines the behavior of the cast. + /// - `filter`: A [`SpatialQueryFilter`] that determines which entities are included in the cast. + /// - `predicate`: A function called on each entity hit by the shape. The shape keeps travelling until the predicate returns `false`. + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "2d")] + /// # use avian2d::prelude::*; + /// # #[cfg(feature = "3d")] + /// use avian3d::prelude::*; + /// use bevy::prelude::*; + /// + /// #[derive(Component)] + /// struct Invisible; + /// + /// # #[cfg(all(feature = "3d", feature = "f32"))] + /// fn print_hits(spatial_query: SpatialQuery, query: Query<&Invisible>) { + /// // Shape properties + /// let shape = Collider::sphere(0.5); + /// let origin = Vec3::ZERO; + /// let rotation = Quat::default(); + /// let direction = Dir3::X; + /// + /// // Configuration for the shape cast + /// let config = ShapeCastConfig::from_max_distance(100.0); + /// let filter = SpatialQueryFilter::default(); + /// + /// // Cast shape and get the first hit that matches the predicate + /// let hit = spatial_query.cast_shape(&shape, origin, rotation, direction, &config, &filter, &|entity| { + /// // Skip entities with the `Invisible` component. + /// !query.contains(entity) + /// }); + /// + /// // Print first hit + /// if let Some(first_hit) = hit { + /// println!("First hit: {:?}", first_hit); + /// } + /// } + /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::cast_ray`] + /// - [`SpatialQuery::ray_hits`] + /// - [`SpatialQuery::ray_hits_callback`] + pub fn cast_shape_predicate( + &self, + shape: &Collider, + origin: Vector, + shape_rotation: RotationValue, + direction: Dir, + config: &ShapeCastConfig, + filter: &SpatialQueryFilter, + predicate: &dyn Fn(Entity) -> bool, + ) -> Option { + self.query_pipeline.cast_shape_predicate( shape, origin, shape_rotation, direction, - max_time_of_impact, - ignore_origin_penetration, - query_filter, + config, + filter, + predicate, ) } /// Casts a [shape](spatial_query#shapecasting) with a given rotation and computes computes all [hits](ShapeHitData) - /// in the order of the time of impact until `max_hits` is reached. + /// in the order of distance until `max_hits` is reached. /// /// # Arguments /// @@ -396,12 +496,9 @@ impl SpatialQuery<'_, '_> { /// - `origin`: Where the shape is cast from. /// - `shape_rotation`: The rotation of the shape being cast. /// - `direction`: What direction the shape is cast in. - /// - `max_time_of_impact`: The maximum distance that the shape can travel. /// - `max_hits`: The maximum number of hits. Additional hits will be missed. - /// - `ignore_origin_penetration`: If true and the shape is already penetrating a collider at the - /// shape origin, the hit will be ignored and only the next hit will be computed. Otherwise, the initial - /// hit will be returned. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `config`: A [`ShapeCastConfig`] that determines the behavior of the cast. + /// - `filter`: A [`SpatialQueryFilter`] that determines which entities are included in the cast. /// - `callback`: A callback function called for each hit. /// /// # Example @@ -415,17 +512,18 @@ impl SpatialQuery<'_, '_> { /// /// # #[cfg(all(feature = "3d", feature = "f32"))] /// fn print_hits(spatial_query: SpatialQuery) { - /// // Cast shape and get all hits - /// let hits = spatial_query.shape_hits( - /// &Collider::sphere(0.5), // Shape - /// Vec3::ZERO, // Origin - /// Quat::default(), // Shape rotation - /// Dir3::X, // Direction - /// 100.0, // Maximum time of impact (travel distance) - /// 20, // Max hits - /// true, // Should initial penetration at the origin be ignored - /// &SpatialQueryFilter::default(), // Query filter - /// ); + /// // Shape properties + /// let shape = Collider::sphere(0.5); + /// let origin = Vec3::ZERO; + /// let rotation = Quat::default(); + /// let direction = Dir3::X; + /// + /// // Configuration for the shape cast + /// let config = ShapeCastConfig::from_max_distance(100.0); + /// let filter = SpatialQueryFilter::default(); + /// + /// // Cast shape and get up to 20 hits + /// let hits = spatial_query.cast_shape(&shape, origin, rotation, direction, 20, &config, &filter); /// /// // Print hits /// for hit in hits.iter() { @@ -433,6 +531,12 @@ impl SpatialQuery<'_, '_> { /// } /// } /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::cast_shape`] + /// - [`SpatialQuery::cast_shape_predicate`] + /// - [`SpatialQuery::shape_hits_callback`] #[allow(clippy::too_many_arguments)] pub fn shape_hits( &self, @@ -440,25 +544,23 @@ impl SpatialQuery<'_, '_> { origin: Vector, shape_rotation: RotationValue, direction: Dir, - max_time_of_impact: Scalar, max_hits: u32, - ignore_origin_penetration: bool, - query_filter: &SpatialQueryFilter, + config: &ShapeCastConfig, + filter: &SpatialQueryFilter, ) -> Vec { self.query_pipeline.shape_hits( shape, origin, shape_rotation, direction, - max_time_of_impact, max_hits, - ignore_origin_penetration, - query_filter, + config, + filter, ) } /// Casts a [shape](spatial_query#shapecasting) with a given rotation and computes computes all [hits](ShapeHitData) - /// in the order of the time of impact, calling the given `callback` for each hit. The shapecast stops when + /// in the order of distance, calling the given `callback` for each hit. The shapecast stops when /// `callback` returns false or all hits have been found. /// /// # Arguments @@ -467,11 +569,8 @@ impl SpatialQuery<'_, '_> { /// - `origin`: Where the shape is cast from. /// - `shape_rotation`: The rotation of the shape being cast. /// - `direction`: What direction the shape is cast in. - /// - `max_time_of_impact`: The maximum distance that the shape can travel. - /// - `ignore_origin_penetration`: If true and the shape is already penetrating a collider at the - /// shape origin, the hit will be ignored and only the next hit will be computed. Otherwise, the initial - /// hit will be returned. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `config`: A [`ShapeCastConfig`] that determines the behavior of the cast. + /// - `filter`: A [`SpatialQueryFilter`] that determines which entities are included in the cast. /// - `callback`: A callback function called for each hit. /// /// # Example @@ -485,22 +584,22 @@ impl SpatialQuery<'_, '_> { /// /// # #[cfg(all(feature = "3d", feature = "f32"))] /// fn print_hits(spatial_query: SpatialQuery) { - /// let mut hits = vec![]; + /// // Shape properties + /// let shape = Collider::sphere(0.5); + /// let origin = Vec3::ZERO; + /// let rotation = Quat::default(); + /// let direction = Dir3::X; /// - /// // Cast shape and get all hits - /// spatial_query.shape_hits_callback( - /// &Collider::sphere(0.5), // Shape - /// Vec3::ZERO, // Origin - /// Quat::default(), // Shape rotation - /// Dir3::X, // Direction - /// 100.0, // Maximum time of impact (travel distance) - /// true, // Should initial penetration at the origin be ignored - /// &SpatialQueryFilter::default(), // Query filter - /// |hit| { // Callback function - /// hits.push(hit); - /// true - /// }, - /// ); + /// // Configuration for the shape cast + /// let config = ShapeCastConfig::from_max_distance(100.0); + /// let filter = SpatialQueryFilter::default(); + /// + /// // Cast shape and get up to 20 hits + /// let mut hits = vec![]; + /// spatial_query.shape_hits_callback(&shape, origin, rotation, direction, 20, &config, &filter, |hit| { + /// hits.push(hit); + /// true + /// }); /// /// // Print hits /// for hit in hits.iter() { @@ -508,6 +607,12 @@ impl SpatialQuery<'_, '_> { /// } /// } /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::cast_shape`] + /// - [`SpatialQuery::cast_shape_predicate`] + /// - [`SpatialQuery::shape_hits`] #[allow(clippy::too_many_arguments)] pub fn shape_hits_callback( &self, @@ -515,9 +620,8 @@ impl SpatialQuery<'_, '_> { origin: Vector, shape_rotation: RotationValue, direction: Dir, - max_time_of_impact: Scalar, - ignore_origin_penetration: bool, - query_filter: &SpatialQueryFilter, + config: &ShapeCastConfig, + filter: &SpatialQueryFilter, callback: impl FnMut(ShapeHitData) -> bool, ) { self.query_pipeline.shape_hits_callback( @@ -525,9 +629,8 @@ impl SpatialQuery<'_, '_> { origin, shape_rotation, direction, - max_time_of_impact, - ignore_origin_penetration, - query_filter, + config, + filter, callback, ) } @@ -563,14 +666,71 @@ impl SpatialQuery<'_, '_> { /// } /// } /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::project_point_predicate`] pub fn project_point( &self, point: Vector, solid: bool, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, + ) -> Option { + self.query_pipeline.project_point(point, solid, filter) + } + + /// Finds the [projection](spatial_query#point-projection) of a given point on the closest [collider](Collider). + /// If one isn't found, `None` is returned. + /// + /// # Arguments + /// + /// - `point`: The point that should be projected. + /// - `solid`: If true and the point is inside of a collider, the projection will be at the point. + /// Otherwise, the collider will be treated as hollow, and the projection will be at the collider's boundary. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `predicate`: A function for filtering which entities are considered in the query. The projection will be on the closest collider that passes the predicate. + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "2d")] + /// # use avian2d::prelude::*; + /// # #[cfg(feature = "3d")] + /// use avian3d::prelude::*; + /// use bevy::prelude::*; + /// + /// #[derive(Component)] + /// struct Invisible; + /// + /// # #[cfg(all(feature = "3d", feature = "f32"))] + /// fn print_point_projection(spatial_query: SpatialQuery, query: Query<&Invisible>) { + /// // Project a point and print the result + /// if let Some(projection) = spatial_query.project_point_predicate( + /// Vec3::ZERO, // Point + /// true, // Are colliders treated as "solid" + /// SpatialQueryFilter::default(), // Query filter + /// &|entity| { // Predicate + /// // Skip entities with the `Invisible` component. + /// !query.contains(entity) + /// } + /// ) { + /// println!("Projection: {:?}", projection); + /// } + /// } + /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::project_point`] + pub fn project_point_predicate( + &self, + point: Vector, + solid: bool, + filter: &SpatialQueryFilter, + predicate: &dyn Fn(Entity) -> bool, ) -> Option { self.query_pipeline - .project_point(point, solid, query_filter) + .project_point_predicate(point, solid, filter, predicate) } /// An [intersection test](spatial_query#intersection-tests) that finds all entities with a [collider](Collider) @@ -579,7 +739,7 @@ impl SpatialQuery<'_, '_> { /// # Arguments /// /// - `point`: The point that intersections are tested against. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// /// # Example /// @@ -600,12 +760,12 @@ impl SpatialQuery<'_, '_> { /// } /// } /// ``` - pub fn point_intersections( - &self, - point: Vector, - query_filter: &SpatialQueryFilter, - ) -> Vec { - self.query_pipeline.point_intersections(point, query_filter) + /// + /// # Related Methods + /// + /// - [`SpatialQuery::point_intersections_callback`] + pub fn point_intersections(&self, point: Vector, filter: &SpatialQueryFilter) -> Vec { + self.query_pipeline.point_intersections(point, filter) } /// An [intersection test](spatial_query#intersection-tests) that finds all entities with a [collider](Collider) @@ -615,7 +775,7 @@ impl SpatialQuery<'_, '_> { /// # Arguments /// /// - `point`: The point that intersections are tested against. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// - `callback`: A callback function called for each intersection. /// /// # Example @@ -645,14 +805,18 @@ impl SpatialQuery<'_, '_> { /// } /// } /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::point_intersections`] pub fn point_intersections_callback( &self, point: Vector, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, callback: impl FnMut(Entity) -> bool, ) { self.query_pipeline - .point_intersections_callback(point, query_filter, callback) + .point_intersections_callback(point, filter, callback) } /// An [intersection test](spatial_query#intersection-tests) that finds all entities with a [`ColliderAabb`] @@ -677,6 +841,10 @@ impl SpatialQuery<'_, '_> { /// } /// } /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::aabb_intersections_with_aabb_callback`] pub fn aabb_intersections_with_aabb(&self, aabb: ColliderAabb) -> Vec { self.query_pipeline.aabb_intersections_with_aabb(aabb) } @@ -711,6 +879,10 @@ impl SpatialQuery<'_, '_> { /// } /// } /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::aabb_intersections_with_aabb`] pub fn aabb_intersections_with_aabb_callback( &self, aabb: ColliderAabb, @@ -728,7 +900,7 @@ impl SpatialQuery<'_, '_> { /// - `shape`: The shape that intersections are tested against represented as a [`Collider`]. /// - `shape_position`: The position of the shape. /// - `shape_rotation`: The rotation of the shape. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// /// # Example /// @@ -753,15 +925,19 @@ impl SpatialQuery<'_, '_> { /// } /// } /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::shape_intersections_callback`] pub fn shape_intersections( &self, shape: &Collider, shape_position: Vector, shape_rotation: RotationValue, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, ) -> Vec { self.query_pipeline - .shape_intersections(shape, shape_position, shape_rotation, query_filter) + .shape_intersections(shape, shape_position, shape_rotation, filter) } /// An [intersection test](spatial_query#intersection-tests) that finds all entities with a [`Collider`] @@ -773,7 +949,7 @@ impl SpatialQuery<'_, '_> { /// - `shape`: The shape that intersections are tested against represented as a [`Collider`]. /// - `shape_position`: The position of the shape. /// - `shape_rotation`: The rotation of the shape. - /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. + /// - `filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// - `callback`: A callback function called for each intersection. /// /// # Example @@ -805,19 +981,23 @@ impl SpatialQuery<'_, '_> { /// } /// } /// ``` + /// + /// # Related Methods + /// + /// - [`SpatialQuery::shape_intersections`] pub fn shape_intersections_callback( &self, shape: &Collider, shape_position: Vector, shape_rotation: RotationValue, - query_filter: &SpatialQueryFilter, + filter: &SpatialQueryFilter, callback: impl FnMut(Entity) -> bool, ) { self.query_pipeline.shape_intersections_callback( shape, shape_position, shape_rotation, - query_filter, + filter, callback, ) }