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, ) }