From 4cb06cb448073d682d5d9a1a40aaf68b3f7dfc21 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sat, 7 Sep 2024 20:31:41 +0300 Subject: [PATCH 1/7] Use config structs for ray casts and shape casts --- crates/avian3d/examples/cast_ray_predicate.rs | 13 +- src/spatial_query/pipeline.rs | 254 ++++----- src/spatial_query/query_filter.rs | 8 +- src/spatial_query/ray_caster.rs | 42 ++ src/spatial_query/shape_caster.rs | 62 ++ src/spatial_query/system_param.rs | 531 +++++++++++------- 6 files changed, 564 insertions(+), 346 deletions(-) diff --git a/crates/avian3d/examples/cast_ray_predicate.rs b/crates/avian3d/examples/cast_ray_predicate.rs index f7595f3b..74caf14e 100644 --- a/crates/avian3d/examples/cast_ray_predicate.rs +++ b/crates/avian3d/examples/cast_ray_predicate.rs @@ -166,19 +166,14 @@ fn raycast( 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, &RayCastConfig::default(), &|entity| { if let Ok((_, out_of_glass)) = cubes.get(entity) { return !out_of_glass.0; // only look at cubes not out of glass } true // if the collider has no OutOfGlass component, then check it nevertheless - }, - ) { + }) + { // set color of hit object to red if let Ok((material_handle, _)) = cubes.get(ray_hit_data.entity) { if let Some(material) = materials.get_mut(material_handle) { diff --git a/src/spatial_query/pipeline.rs b/src/spatial_query/pipeline.rs index 9055e850..8a5e293f 100644 --- a/src/spatial_query/pipeline.rs +++ b/src/spatial_query/pipeline.rs @@ -155,28 +155,20 @@ 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. + /// - `config`: A [`RayCastConfig`] that determines the behavior of the cast. + /// + /// ## Related Methods /// - /// See also: [`SpatialQuery::cast_ray`] + /// - [`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, - solid: bool, - query_filter: &SpatialQueryFilter, + config: &RayCastConfig, ) -> Option { - self.cast_ray_predicate( - origin, - direction, - max_time_of_impact, - solid, - query_filter, - &|_| true, - ) + self.cast_ray_predicate(origin, direction, config, &|_| true) } /// Casts a [ray](spatial_query#raycasting) and computes the closest [hit](RayHitData) with a collider. @@ -186,30 +178,28 @@ 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. + /// - `config`: A [`RayCastConfig`] that determines the behavior of the cast. + /// - `predicate`: A function called on each entity hit by the ray. The ray keeps travelling until the predicate returns `false`. + /// + /// ## Related Methods /// - /// See also: [`SpatialQuery::cast_ray`] + /// - [`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, - solid: bool, - query_filter: &SpatialQueryFilter, + config: &RayCastConfig, 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(&config.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, - solid, + config.max_distance, + config.solid, ); self.qbvh @@ -230,34 +220,26 @@ 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_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. + /// - `config`: A [`RayCastConfig`] that determines the behavior of the cast. /// - /// See also: [`SpatialQuery::ray_hits`] + /// ## 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_hits: u32, - solid: bool, - query_filter: &SpatialQueryFilter, + config: &RayCastConfig, ) -> 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, config, |hit| { + hits.push(hit); + (hits.len() as u32) < max_hits + }); hits } @@ -270,20 +252,19 @@ 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. + /// - `config`: A [`RayCastConfig`] that determines the behavior of the cast. /// - `callback`: A callback function called for each hit. /// - /// See also: [`SpatialQuery::ray_hits_callback`] + /// ## 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, - solid: bool, - query_filter: &SpatialQueryFilter, + config: &RayCastConfig, mut callback: impl FnMut(RayHitData) -> bool, ) { let colliders = &self.colliders; @@ -293,12 +274,12 @@ 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 config.filter.test(entity, *layers) { if let Some(hit) = shape.shape_scaled().cast_ray_and_get_normal( iso, &ray, - max_time_of_impact, - solid, + config.max_distance, + config.solid, ) { let hit = RayHitData { entity, @@ -314,7 +295,7 @@ impl SpatialQueryPipeline { }; let mut visitor = - RayIntersectionsVisitor::new(&ray, max_time_of_impact, &mut leaf_callback); + RayIntersectionsVisitor::new(&ray, config.max_distance, &mut leaf_callback); self.qbvh.traverse_depth_first(&mut visitor); } @@ -329,13 +310,13 @@ 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. + /// + /// ## Related Methods /// - /// See also: [`SpatialQuery::cast_shape`] + /// - [`SpatialQuery::cast_shape_predicate`] + /// - [`SpatialQuery::shape_hits`] + /// - [`SpatialQuery::shape_hits_callback`] #[allow(clippy::too_many_arguments)] pub fn cast_shape( &self, @@ -343,20 +324,9 @@ impl SpatialQueryPipeline { origin: Vector, shape_rotation: RotationValue, direction: Dir, - max_time_of_impact: Scalar, - ignore_origin_penetration: bool, - query_filter: &SpatialQueryFilter, + config: &ShapeCastConfig, ) -> Option { - self.cast_shape_predicate( - shape, - origin, - shape_rotation, - direction, - max_time_of_impact, - ignore_origin_penetration, - query_filter, - &|_| true, - ) + self.cast_shape_predicate(shape, origin, shape_rotation, direction, config, &|_| true) } /// Casts a [shape](spatial_query#shapecasting) with a given rotation and computes the closest [hit](ShapeHits) @@ -370,15 +340,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. - /// - `predicate`: A function with which the colliders are filtered. Given the Entity it should return false, if the - /// entity should be ignored. + /// - `config`: A [`ShapeCastConfig`] that determines the behavior of the cast. + /// - `predicate`: A function called on each entity hit by the shape. The shape keeps travelling until the predicate returns `false`. /// - /// See also: [`SpatialQuery::cast_shape`] + /// ## Related Methods + /// + /// - [`SpatialQuery::cast_shape`] + /// - [`SpatialQuery::shape_hits`] + /// - [`SpatialQuery::shape_hits_callback`] #[allow(clippy::too_many_arguments)] pub fn cast_shape_predicate( &self, @@ -386,9 +355,7 @@ impl SpatialQueryPipeline { origin: Vector, shape_rotation: RotationValue, direction: Dir, - max_time_of_impact: Scalar, - ignore_origin_penetration: bool, - query_filter: &SpatialQueryFilter, + config: &ShapeCastConfig, predicate: &dyn Fn(Entity) -> bool, ) -> Option { let rotation: Rotation; @@ -403,7 +370,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(&config.filter, predicate); let mut visitor = TOICompositeShapeShapeBestFirstVisitor::new( &*self.dispatcher, &shape_isometry, @@ -411,8 +378,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_penetration, + compute_impact_geometry_on_penetration: config.compute_impact_on_penetration, ..default() }, ); @@ -438,15 +406,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. /// - `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. /// - /// See also: [`SpatialQuery::shape_hits`] + /// ## Related Methods + /// + /// - [`SpatialQuery::cast_shape`] + /// - [`SpatialQuery::cast_shape_predicate`] + /// - [`SpatialQuery::shape_hits_callback`] #[allow(clippy::too_many_arguments)] pub fn shape_hits( &self, @@ -454,25 +421,14 @@ 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, ) -> Vec { let mut hits = Vec::with_capacity(10); - self.shape_hits_callback( - shape, - origin, - shape_rotation, - direction, - max_time_of_impact, - ignore_origin_penetration, - query_filter, - |hit| { - hits.push(hit); - (hits.len() as u32) < max_hits - }, - ); + self.shape_hits_callback(shape, origin, shape_rotation, direction, config, |hit| { + hits.push(hit); + (hits.len() as u32) < max_hits + }); hits } @@ -486,14 +442,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. /// - `callback`: A callback function called for each hit. /// - /// See also: [`SpatialQuery::shape_hits_callback`] + /// ## Related Methods + /// + /// - [`SpatialQuery::cast_shape`] + /// - [`SpatialQuery::cast_shape_predicate`] + /// - [`SpatialQuery::shape_hits`] #[allow(clippy::too_many_arguments)] pub fn shape_hits_callback( &self, @@ -501,15 +457,20 @@ impl SpatialQueryPipeline { origin: Vector, shape_rotation: RotationValue, direction: Dir, - max_time_of_impact: Scalar, - ignore_origin_penetration: bool, - query_filter: &SpatialQueryFilter, + config: &ShapeCastConfig, 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 = config.filter.clone(); + + let shape_cast_options = ShapeCastOptions { + max_time_of_impact: config.max_distance, + target_distance: config.target_distance, + stop_at_penetration: !config.ignore_penetration, + compute_impact_geometry_on_penetration: config.compute_impact_on_penetration, + }; let rotation: Rotation; #[cfg(feature = "2d")] @@ -532,11 +493,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) = @@ -572,7 +529,9 @@ impl SpatialQueryPipeline { /// 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. /// - /// See also: [`SpatialQuery::project_point`] + /// ## Related Methods + /// + /// - [`SpatialQuery::project_point_predicate`] pub fn project_point( &self, point: Vector, @@ -591,10 +550,11 @@ impl SpatialQueryPipeline { /// - `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. + /// - `predicate`: A function for filtering which entities are considered in the query. The projection will be on the closest collider that passes the predicate. /// - /// See also: [`SpatialQuery::project_point`] + /// ## Related Methods + /// + /// - [`SpatialQuery::project_point`] pub fn project_point_predicate( &self, point: Vector, @@ -624,7 +584,9 @@ impl SpatialQueryPipeline { /// - `point`: The point that intersections are tested against. /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// - /// See also: [`SpatialQuery::point_intersections`] + /// ## Related Methods + /// + /// - [`SpatialQuery::point_intersections_callback`] pub fn point_intersections( &self, point: Vector, @@ -648,7 +610,9 @@ impl SpatialQueryPipeline { /// - `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 + /// + /// - [`SpatialQuery::point_intersections`] pub fn point_intersections_callback( &self, point: Vector, @@ -676,7 +640,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 + /// + /// - [`SpatialQuery::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 +656,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 + /// + /// - [`SpatialQuery::aabb_intersections_with_aabb`] pub fn aabb_intersections_with_aabb_callback( &self, aabb: ColliderAabb, @@ -721,7 +689,9 @@ impl SpatialQueryPipeline { /// - `shape_rotation`: The rotation of the shape. /// - `query_filter`: A [`SpatialQueryFilter`] that determines which colliders are taken into account in the query. /// - /// See also: [`SpatialQuery::shape_intersections`] + /// ## Related Methods + /// + /// - [`SpatialQuery::shape_intersections_callback`] pub fn shape_intersections( &self, shape: &Collider, @@ -755,7 +725,9 @@ impl SpatialQueryPipeline { /// - `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 + /// + /// - [`SpatialQuery::shape_intersections`] pub fn shape_intersections_callback( &self, shape: &Collider, diff --git a/src/spatial_query/query_filter.rs b/src/spatial_query/query_filter.rs index af9d992b..3f9f0290 100644 --- a/src/spatial_query/query_filter.rs +++ b/src/spatial_query/query_filter.rs @@ -1,4 +1,4 @@ -use bevy::{prelude::*, utils::HashSet}; +use bevy::{prelude::*, utils::EntityHashSet}; use crate::prelude::*; @@ -36,7 +36,7 @@ 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 { @@ -61,7 +61,7 @@ impl SpatialQueryFilter { /// Creates a new [`SpatialQueryFilter`] with the given entities excluded from the [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() } } @@ -75,7 +75,7 @@ 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 } diff --git a/src/spatial_query/ray_caster.rs b/src/spatial_query/ray_caster.rs index 72944aa2..a7458f1d 100644 --- a/src/spatial_query/ray_caster.rs +++ b/src/spatial_query/ray_caster.rs @@ -77,35 +77,44 @@ 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, + /// 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. 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. pub query_filter: SpatialQueryFilter, } @@ -341,6 +350,39 @@ impl Component for RayCaster { } } +/// Configuration for a ray 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 RayCastConfig { + /// The maximum distance the ray can travel. + /// + /// By default, this is infinite. + #[doc(alias = "max_time_of_impact")] + pub max_distance: Scalar, + + /// 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. + /// + /// By default, this is `true`. + pub solid: bool, + + /// A filter for configuring which entities are included in the spatial query. + pub filter: SpatialQueryFilter, +} + +impl Default for RayCastConfig { + fn default() -> Self { + Self { + max_distance: Scalar::MAX, + solid: true, + filter: SpatialQueryFilter::default(), + } + } +} + /// Contains the hits of a ray cast by a [`RayCaster`]. /// /// The maximum number of hits depends on the value of `max_hits` in [`RayCaster`]. diff --git a/src/spatial_query/shape_caster.rs b/src/spatial_query/shape_caster.rs index 6c6e07d3..c990ea0a 100644 --- a/src/spatial_query/shape_caster.rs +++ b/src/spatial_query/shape_caster.rs @@ -60,45 +60,57 @@ 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. /// @@ -106,8 +118,10 @@ pub struct ShapeCaster { /// 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 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. pub query_filter: SpatialQueryFilter, } @@ -368,6 +382,54 @@ impl Component for ShapeCaster { } } +/// 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_impact_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_penetration: bool, + + /// A filter for configuring which entities are included in the spatial query. + pub filter: SpatialQueryFilter, +} + +impl Default for ShapeCastConfig { + fn default() -> Self { + Self { + max_distance: Scalar::MAX, + target_distance: 0.0, + compute_impact_on_penetration: true, + ignore_penetration: false, + filter: SpatialQueryFilter::default(), + } + } +} + /// Contains the hits of a shape cast by a [`ShapeCaster`]. The hits are in the order of time of impact. /// /// The maximum number of hits depends on the value of `max_hits` in [`ShapeCaster`]. By default only diff --git a/src/spatial_query/system_param.rs b/src/spatial_query/system_param.rs index 2a9401ab..968a7f5f 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,23 @@ use bevy::{ecs::system::SystemParam, prelude::*}; /// /// # #[cfg(all(feature = "3d", feature = "f32"))] /// fn print_hits(spatial_query: SpatialQuery) { +/// // Configuration for the ray cast +/// let config = RayCastConfig { +/// max_distance: 100.0, +/// ..default() +/// }; +/// +/// // Ray origin and direction +/// let origin = Vec3::ZERO; +/// let direction = Dir3::X; +/// /// // 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, &config) { /// 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, 20, &config); /// /// // Print hits /// for hit in hits.iter() { @@ -93,10 +90,7 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// /// - `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. + /// - `config`: A [`RayCastConfig`] that determines the behavior of the cast. /// /// ## Example /// @@ -109,28 +103,35 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// /// # #[cfg(all(feature = "3d", feature = "f32"))] /// fn print_hits(spatial_query: SpatialQuery) { + /// // Configuration for the ray cast + /// let config = RayCastConfig { + /// max_distance: 100.0, + /// ..default() + /// }; + /// + /// // Ray origin and direction + /// let origin = Vec3::ZERO; + /// let direction = Dir3::X; + /// /// // 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, &config) { /// 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, - solid: bool, - query_filter: &SpatialQueryFilter, + config: &RayCastConfig, ) -> Option { - self.query_pipeline - .cast_ray(origin, direction, max_time_of_impact, solid, query_filter) + self.query_pipeline.cast_ray(origin, direction, config) } /// Casts a [ray](spatial_query#raycasting) and computes the closest [hit](RayHitData) with a collider. @@ -140,12 +141,8 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// /// - `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. + /// - `config`: A [`RayCastConfig`] that determines the behavior of the cast. + /// - `predicate`: A function called on each entity hit by the ray. The ray keeps travelling until the predicate returns `false`. /// /// ## Example /// @@ -161,39 +158,43 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// /// # #[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) - /// } - /// ) { + /// // Configuration for the ray cast + /// let config = RayCastConfig { + /// max_distance: 100.0, + /// ..default() + /// }; + /// + /// // Ray origin and direction + /// let origin = Vec3::ZERO; + /// let direction = Dir3::X; + /// + /// // Cast ray and get the first hit that matches the predicate + /// let hit = spatial_query.cast_ray_predicate(origin, direction, &config, &|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, - solid: bool, - query_filter: &SpatialQueryFilter, + config: &RayCastConfig, predicate: &dyn Fn(Entity) -> bool, ) -> Option { - self.query_pipeline.cast_ray_predicate( - origin, - direction, - max_time_of_impact, - solid, - query_filter, - predicate, - ) + self.query_pipeline + .cast_ray_predicate(origin, direction, config, predicate) } /// Casts a [ray](spatial_query#raycasting) and computes all [hits](RayHitData) until `max_hits` is reached. @@ -205,11 +206,8 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// /// - `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_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. + /// - `config`: A [`RayCastConfig`] that determines the behavior of the cast. /// /// ## Example /// @@ -222,15 +220,18 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// /// # #[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 - /// ); + /// // Configuration for the ray cast + /// let config = RayCastConfig { + /// max_distance: 100.0, + /// ..default() + /// }; + /// + /// // Ray origin and direction + /// let origin = Vec3::ZERO; + /// let direction = Dir3::X; + /// + /// // Cast ray and get up to 20 hits + /// let hits = spatial_query.ray_hits(origin, direction, 20, &config); /// /// // Print hits /// for hit in hits.iter() { @@ -238,23 +239,21 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// } /// } /// ``` + /// + /// ## 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_hits: u32, - solid: bool, - query_filter: &SpatialQueryFilter, + config: &RayCastConfig, ) -> 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_hits, config) } /// Casts a [ray](spatial_query#raycasting) and computes all [hits](RayHitData), calling the given `callback` @@ -266,10 +265,7 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// /// - `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. + /// - `config`: A [`RayCastConfig`] that determines the behavior of the cast. /// - `callback`: A callback function called for each hit. /// /// ## Example @@ -283,20 +279,22 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// /// # #[cfg(all(feature = "3d", feature = "f32"))] /// fn print_hits(spatial_query: SpatialQuery) { - /// let mut hits = vec![]; + /// // Configuration for the ray cast + /// let config = RayCastConfig { + /// max_distance: 100.0, + /// ..default() + /// }; + /// + /// // Ray origin and direction + /// let origin = Vec3::ZERO; + /// let direction = Dir3::X; /// /// // 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, 20, &config, |hit| { + /// hits.push(hit); + /// true + /// }); /// /// // Print hits /// for hit in hits.iter() { @@ -304,26 +302,24 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// } /// } /// ``` + /// + /// ## 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, - solid: bool, - query_filter: &SpatialQueryFilter, + config: &RayCastConfig, callback: impl FnMut(RayHitData) -> bool, ) { - self.query_pipeline.ray_hits_callback( - origin, - direction, - max_time_of_impact, - solid, - query_filter, - callback, - ) + self.query_pipeline + .ray_hits_callback(origin, direction, config, 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 +330,7 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// - `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. /// /// ## Example /// @@ -351,20 +343,31 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// /// # #[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 - /// ) { + /// // Configuration for the shape cast + /// let config = ShapeCastConfig { + /// max_distance: 100.0, + /// ..default() + /// }; + /// + /// // Shape properties + /// let shape = Collider::sphere(0.5); + /// let origin = Vec3::ZERO; + /// let rotation = Quat::default(); + /// let direction = Dir3::X; + /// + /// // Cast shape and print first hit + /// if let Some(first_hit) = spatial_query.cast_shape(&shape, origin, rotation, direction, &config) + /// { /// 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,18 +375,85 @@ impl<'w, 's> SpatialQuery<'w, 's> { origin: Vector, shape_rotation: RotationValue, direction: Dir, - max_time_of_impact: Scalar, - ignore_origin_penetration: bool, - query_filter: &SpatialQueryFilter, + config: &ShapeCastConfig, + ) -> Option { + self.query_pipeline + .cast_shape(shape, origin, shape_rotation, direction, config) + } + + /// 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. + /// - `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>) { + /// // Configuration for the shape cast + /// let config = ShapeCastConfig { + /// max_distance: 100.0, + /// ..default() + /// }; + /// + /// // Shape properties + /// let shape = Collider::sphere(0.5); + /// let origin = Vec3::ZERO; + /// let rotation = Quat::default(); + /// let direction = Dir3::X; + /// + /// // Cast shape and get the first hit that matches the predicate + /// let hit = spatial_query.cast_shape(&shape, origin, rotation, direction, &config, &|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, + predicate: &dyn Fn(Entity) -> bool, ) -> Option { - self.query_pipeline.cast_shape( + self.query_pipeline.cast_shape_predicate( shape, origin, shape_rotation, direction, - max_time_of_impact, - ignore_origin_penetration, - query_filter, + config, + predicate, ) } @@ -396,12 +466,8 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// - `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. /// - `callback`: A callback function called for each hit. /// /// ## Example @@ -415,17 +481,20 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// /// # #[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 - /// ); + /// // Configuration for the shape cast + /// let config = ShapeCastConfig { + /// max_distance: 100.0, + /// ..default() + /// }; + /// + /// // Shape properties + /// let shape = Collider::sphere(0.5); + /// let origin = Vec3::ZERO; + /// let rotation = Quat::default(); + /// let direction = Dir3::X; + /// + /// // Cast shape and get up to 20 hits + /// let hits = spatial_query.cast_shape(&shape, origin, rotation, direction, 20, &config); /// /// // Print hits /// for hit in hits.iter() { @@ -433,6 +502,12 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// } /// } /// ``` + /// + /// ## Related Methods + /// + /// - [`SpatialQuery::cast_shape`] + /// - [`SpatialQuery::cast_shape_predicate`] + /// - [`SpatialQuery::shape_hits_callback`] #[allow(clippy::too_many_arguments)] pub fn shape_hits( &self, @@ -440,21 +515,11 @@ impl<'w, 's> SpatialQuery<'w, 's> { origin: Vector, shape_rotation: RotationValue, direction: Dir, - max_time_of_impact: Scalar, max_hits: u32, - ignore_origin_penetration: bool, - query_filter: &SpatialQueryFilter, + config: &ShapeCastConfig, ) -> Vec { - self.query_pipeline.shape_hits( - shape, - origin, - shape_rotation, - direction, - max_time_of_impact, - max_hits, - ignore_origin_penetration, - query_filter, - ) + self.query_pipeline + .shape_hits(shape, origin, shape_rotation, direction, max_hits, config) } /// Casts a [shape](spatial_query#shapecasting) with a given rotation and computes computes all [hits](ShapeHitData) @@ -467,11 +532,7 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// - `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. /// - `callback`: A callback function called for each hit. /// /// ## Example @@ -485,22 +546,24 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// /// # #[cfg(all(feature = "3d", feature = "f32"))] /// fn print_hits(spatial_query: SpatialQuery) { + /// // Configuration for the shape cast + /// let config = ShapeCastConfig { + /// max_distance: 100.0, + /// ..default() + /// }; + /// + /// // Shape properties + /// let shape = Collider::sphere(0.5); + /// let origin = Vec3::ZERO; + /// let rotation = Quat::default(); + /// let direction = Dir3::X; + /// + /// // Cast shape and get up to 20 hits /// let mut hits = vec![]; - /// - /// // 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 - /// }, - /// ); + /// spatial_query.shape_hits_callback(&shape, origin, rotation, direction, 20, &config, |hit| { + /// hits.push(hit); + /// true + /// }); /// /// // Print hits /// for hit in hits.iter() { @@ -508,6 +571,12 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// } /// } /// ``` + /// + /// ## 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 +584,7 @@ impl<'w, 's> SpatialQuery<'w, 's> { origin: Vector, shape_rotation: RotationValue, direction: Dir, - max_time_of_impact: Scalar, - ignore_origin_penetration: bool, - query_filter: &SpatialQueryFilter, + config: &ShapeCastConfig, callback: impl FnMut(ShapeHitData) -> bool, ) { self.query_pipeline.shape_hits_callback( @@ -525,9 +592,7 @@ impl<'w, 's> SpatialQuery<'w, 's> { origin, shape_rotation, direction, - max_time_of_impact, - ignore_origin_penetration, - query_filter, + config, callback, ) } @@ -563,6 +628,10 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// } /// } /// ``` + /// + /// ## Related Methods + /// + /// - [`SpatialQuery::project_point_predicate`] pub fn project_point( &self, point: Vector, @@ -573,6 +642,60 @@ impl<'w, 's> SpatialQuery<'w, 's> { .project_point(point, solid, query_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. + /// - `query_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, + query_filter: &SpatialQueryFilter, + predicate: &dyn Fn(Entity) -> bool, + ) -> Option { + self.query_pipeline + .project_point_predicate(point, solid, query_filter, predicate) + } + /// An [intersection test](spatial_query#intersection-tests) that finds all entities with a [collider](Collider) /// that contains the given point. /// @@ -600,6 +723,10 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// } /// } /// ``` + /// + /// ## Related Methods + /// + /// - [`SpatialQuery::point_intersections_callback`] pub fn point_intersections( &self, point: Vector, @@ -645,6 +772,10 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// } /// } /// ``` + /// + /// ## Related Methods + /// + /// - [`SpatialQuery::point_intersections`] pub fn point_intersections_callback( &self, point: Vector, @@ -677,6 +808,10 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// } /// } /// ``` + /// + /// ## 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 +846,10 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// } /// } /// ``` + /// + /// ## Related Methods + /// + /// - [`SpatialQuery::aabb_intersections_with_aabb`] pub fn aabb_intersections_with_aabb_callback( &self, aabb: ColliderAabb, @@ -753,6 +892,10 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// } /// } /// ``` + /// + /// ## Related Methods + /// + /// - [`SpatialQuery::shape_intersections_callback`] pub fn shape_intersections( &self, shape: &Collider, @@ -805,6 +948,10 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// } /// } /// ``` + /// + /// ## Related Methods + /// + /// - [`SpatialQuery::shape_intersections`] pub fn shape_intersections_callback( &self, shape: &Collider, From 855dd4ab9a364d734a77d74f3910d2e9f3cd4835 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sat, 7 Sep 2024 21:45:26 +0300 Subject: [PATCH 2/7] Change most "time of impact" to "distance", add missing config, improve docs --- .../examples/dynamic_character_2d/plugin.rs | 2 +- .../examples/kinematic_character_2d/plugin.rs | 2 +- crates/avian2d/examples/ray_caster.rs | 6 +- crates/avian3d/examples/cast_ray_predicate.rs | 2 +- .../examples/dynamic_character_3d/plugin.rs | 2 +- .../examples/kinematic_character_3d/plugin.rs | 2 +- src/collision/collider/parry/mod.rs | 16 +-- src/debug_render/gizmos.rs | 20 +-- src/debug_render/mod.rs | 4 +- src/spatial_query/mod.rs | 14 +- src/spatial_query/pipeline.rs | 16 +-- src/spatial_query/ray_caster.rs | 87 ++++++------ src/spatial_query/shape_caster.rs | 125 ++++++++++++------ src/spatial_query/system_param.rs | 4 +- 14 files changed, 175 insertions(+), 127 deletions(-) diff --git a/crates/avian2d/examples/dynamic_character_2d/plugin.rs b/crates/avian2d/examples/dynamic_character_2d/plugin.rs index 765e287a..97051d83 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 6b04a0b5..a19b06d9 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 22878df7..ff202776 100644 --- a/crates/avian2d/examples/ray_caster.rs +++ b/crates/avian2d/examples/ray_caster.rs @@ -74,11 +74,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 74caf14e..92aa6c8a 100644 --- a/crates/avian3d/examples/cast_ray_predicate.rs +++ b/crates/avian3d/examples/cast_ray_predicate.rs @@ -182,7 +182,7 @@ fn raycast( } // 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; + 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; } else { diff --git a/crates/avian3d/examples/dynamic_character_3d/plugin.rs b/crates/avian3d/examples/dynamic_character_3d/plugin.rs index b2530581..49a76ef9 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 6cfd576a..f2e7d50b 100644 --- a/crates/avian3d/examples/kinematic_character_3d/plugin.rs +++ b/crates/avian3d/examples/kinematic_character_3d/plugin.rs @@ -125,7 +125,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 f208ac5d..93a2864b 100644 --- a/src/collision/collider/parry/mod.rs +++ b/src/collision/collider/parry/mod.rs @@ -606,16 +606,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( @@ -624,13 +624,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())) @@ -642,19 +642,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 b4189652..8c3cf70f 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, @@ -459,7 +459,7 @@ impl<'w, 's> PhysicsGizmoExt for Gizmos<'w, 's, PhysicsGizmos> { &mut self, origin: Vector, direction: Dir, - max_time_of_impact: Scalar, + max_distance: Scalar, hits: &[RayHitData], ray_color: Color, point_color: Color, @@ -468,8 +468,8 @@ impl<'w, 's> PhysicsGizmoExt for Gizmos<'w, 's, PhysicsGizmos> { ) { let max_toi = 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( @@ -481,7 +481,7 @@ impl<'w, 's> PhysicsGizmoExt for Gizmos<'w, 's, PhysicsGizmos> { // 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")] @@ -516,7 +516,7 @@ impl<'w, 's> PhysicsGizmoExt for Gizmos<'w, 's, 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, @@ -530,8 +530,8 @@ impl<'w, 's> PhysicsGizmoExt for Gizmos<'w, 's, PhysicsGizmos> { let max_toi = 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); @@ -569,7 +569,7 @@ impl<'w, 's> PhysicsGizmoExt for Gizmos<'w, 's, 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 5dcd5ad0..73096705 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/spatial_query/mod.rs b/src/spatial_query/mod.rs index 79f6290b..0007729e 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 long 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 8a5e293f..523fe8ed 100644 --- a/src/spatial_query/pipeline.rs +++ b/src/spatial_query/pipeline.rs @@ -206,7 +206,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(), }) } @@ -283,7 +283,7 @@ impl SpatialQueryPipeline { ) { let hit = RayHitData { entity, - time_of_impact: hit.time_of_impact, + distance: hit.time_of_impact, normal: hit.normal.into(), }; @@ -379,7 +379,7 @@ impl SpatialQueryPipeline { &**shape.shape_scaled(), ShapeCastOptions { max_time_of_impact: config.max_distance, - stop_at_penetration: !config.ignore_penetration, + stop_at_penetration: !config.ignore_origin_penetration, compute_impact_geometry_on_penetration: config.compute_impact_on_penetration, ..default() }, @@ -389,7 +389,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(), @@ -398,7 +398,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 /// @@ -433,7 +433,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 @@ -468,7 +468,7 @@ impl SpatialQueryPipeline { let shape_cast_options = ShapeCastOptions { max_time_of_impact: config.max_distance, target_distance: config.target_distance, - stop_at_penetration: !config.ignore_penetration, + stop_at_penetration: !config.ignore_origin_penetration, compute_impact_geometry_on_penetration: config.compute_impact_on_penetration, }; @@ -501,7 +501,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(), diff --git a/src/spatial_query/ray_caster.rs b/src/spatial_query/ray_caster.rs index a7458f1d..2579ae29 100644 --- a/src/spatial_query/ray_caster.rs +++ b/src/spatial_query/ray_caster.rs @@ -20,8 +20,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 long 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. @@ -29,7 +29,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 @@ -63,7 +63,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, /// ); /// } @@ -94,10 +94,6 @@ pub struct RayCaster { /// 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**. @@ -105,17 +101,23 @@ pub struct RayCaster { /// 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, } @@ -127,7 +129,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, @@ -173,29 +175,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; @@ -266,14 +275,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(), }, ) { @@ -297,19 +306,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(), }; } @@ -324,7 +333,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); } } @@ -362,6 +371,8 @@ pub struct RayCastConfig { #[doc(alias = "max_time_of_impact")] pub max_distance: Scalar, + /// Controls how the ray behaves when the ray origin is inside of a shape. + /// /// 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. @@ -369,7 +380,7 @@ pub struct RayCastConfig { /// By default, this is `true`. pub solid: bool, - /// A filter for configuring which entities are included in the spatial query. + /// Rules that determine which colliders are taken into account in the ray cast. pub filter: SpatialQueryFilter, } @@ -391,7 +402,7 @@ impl Default for RayCastConfig { /// /// 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 @@ -410,11 +421,7 @@ impl Default for RayCastConfig { /// 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); /// } /// } /// } @@ -454,17 +461,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() } } @@ -485,9 +492,9 @@ 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 long 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 c990ea0a..a71f15f3 100644 --- a/src/spatial_query/shape_caster.rs +++ b/src/spatial_query/shape_caster.rs @@ -16,7 +16,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. @@ -104,25 +104,39 @@ pub struct ShapeCaster { /// 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_impact_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, } @@ -146,8 +160,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_impact_on_penetration: true, ignore_origin_penetration: false, ignore_self: true, query_filter: SpatialQueryFilter::default(), @@ -201,10 +217,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_impact_on_penetration(mut self, compute_contact: bool) -> Self { + self.compute_impact_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 { @@ -213,18 +249,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; @@ -331,7 +374,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() }, @@ -340,7 +383,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(), @@ -412,9 +455,9 @@ pub struct ShapeCastConfig { /// the cast will ignore any impact that happens at the cast origin. /// /// The default is `false`. - pub ignore_penetration: bool, + pub ignore_origin_penetration: bool, - /// A filter for configuring which entities are included in the spatial query. + /// Rules that determine which colliders are taken into account in the shape cast. pub filter: SpatialQueryFilter, } @@ -424,13 +467,13 @@ impl Default for ShapeCastConfig { max_distance: Scalar::MAX, target_distance: 0.0, compute_impact_on_penetration: true, - ignore_penetration: false, + ignore_origin_penetration: false, filter: SpatialQueryFilter::default(), } } } -/// Contains the hits of a shape cast by a [`ShapeCaster`]. The hits are in the order of time of impact. +/// 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. @@ -447,11 +490,7 @@ impl Default for ShapeCastConfig { /// 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); /// } /// } /// } @@ -488,7 +527,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() } @@ -510,19 +549,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 long the shape travelled before the initial hit. + #[doc(alias = "time_of_impact")] + pub distance: Scalar, + + /// The closest point on the hit shape at the time of impact, 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 cast shape at the time of impact, 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 968a7f5f..fa37eb6c 100644 --- a/src/spatial_query/system_param.rs +++ b/src/spatial_query/system_param.rs @@ -458,7 +458,7 @@ impl<'w, 's> SpatialQuery<'w, 's> { } /// 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 /// @@ -523,7 +523,7 @@ impl<'w, 's> SpatialQuery<'w, 's> { } /// 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 From 36e441b7061dd14ed1888881892e3b59d5eb9a04 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sat, 7 Sep 2024 21:53:50 +0300 Subject: [PATCH 3/7] Rename `max_toi` variables to `max_distance` --- src/debug_render/gizmos.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/debug_render/gizmos.rs b/src/debug_render/gizmos.rs index 8c3cf70f..de17121e 100644 --- a/src/debug_render/gizmos.rs +++ b/src/debug_render/gizmos.rs @@ -466,7 +466,7 @@ impl<'w, 's> PhysicsGizmoExt for Gizmos<'w, 's, PhysicsGizmos> { normal_color: Color, length_unit: Scalar, ) { - let max_toi = hits + let max_distance = hits .iter() .max_by(|a, b| a.distance.total_cmp(&b.distance)) .map_or(max_distance, |hit| hit.distance); @@ -474,7 +474,7 @@ impl<'w, 's> PhysicsGizmoExt for Gizmos<'w, 's, PhysicsGizmos> { // 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, ); @@ -528,7 +528,7 @@ impl<'w, 's> PhysicsGizmoExt for Gizmos<'w, 's, 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.distance.total_cmp(&b.distance)) .map_or(max_distance, |hit| hit.distance); @@ -540,7 +540,7 @@ impl<'w, 's> PhysicsGizmoExt for Gizmos<'w, 's, 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, ); From 7ddb12a8f5a7250e91faa97b6b8ab986bde0ef94 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sat, 7 Sep 2024 21:58:12 +0300 Subject: [PATCH 4/7] Link to correct methods in `SpatialQueryPipeline` method docs --- src/spatial_query/pipeline.rs | 64 +++++++++++++++++------------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/spatial_query/pipeline.rs b/src/spatial_query/pipeline.rs index 523fe8ed..d5f33358 100644 --- a/src/spatial_query/pipeline.rs +++ b/src/spatial_query/pipeline.rs @@ -159,9 +159,9 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::cast_ray_predicate`] - /// - [`SpatialQuery::ray_hits`] - /// - [`SpatialQuery::ray_hits_callback`] + /// - [`SpatialQueryPipeline::cast_ray_predicate`] + /// - [`SpatialQueryPipeline::ray_hits`] + /// - [`SpatialQueryPipeline::ray_hits_callback`] pub fn cast_ray( &self, origin: Vector, @@ -183,9 +183,9 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::cast_ray`] - /// - [`SpatialQuery::ray_hits`] - /// - [`SpatialQuery::ray_hits_callback`] + /// - [`SpatialQueryPipeline::cast_ray`] + /// - [`SpatialQueryPipeline::ray_hits`] + /// - [`SpatialQueryPipeline::ray_hits_callback`] pub fn cast_ray_predicate( &self, origin: Vector, @@ -225,9 +225,9 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::cast_ray`] - /// - [`SpatialQuery::cast_ray_predicate`] - /// - [`SpatialQuery::ray_hits_callback`] + /// - [`SpatialQueryPipeline::cast_ray`] + /// - [`SpatialQueryPipeline::cast_ray_predicate`] + /// - [`SpatialQueryPipeline::ray_hits_callback`] pub fn ray_hits( &self, origin: Vector, @@ -257,9 +257,9 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::cast_ray`] - /// - [`SpatialQuery::cast_ray_predicate`] - /// - [`SpatialQuery::ray_hits`] + /// - [`SpatialQueryPipeline::cast_ray`] + /// - [`SpatialQueryPipeline::cast_ray_predicate`] + /// - [`SpatialQueryPipeline::ray_hits`] pub fn ray_hits_callback( &self, origin: Vector, @@ -314,9 +314,9 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::cast_shape_predicate`] - /// - [`SpatialQuery::shape_hits`] - /// - [`SpatialQuery::shape_hits_callback`] + /// - [`SpatialQueryPipeline::cast_shape_predicate`] + /// - [`SpatialQueryPipeline::shape_hits`] + /// - [`SpatialQueryPipeline::shape_hits_callback`] #[allow(clippy::too_many_arguments)] pub fn cast_shape( &self, @@ -345,9 +345,9 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::cast_shape`] - /// - [`SpatialQuery::shape_hits`] - /// - [`SpatialQuery::shape_hits_callback`] + /// - [`SpatialQueryPipeline::cast_shape`] + /// - [`SpatialQueryPipeline::shape_hits`] + /// - [`SpatialQueryPipeline::shape_hits_callback`] #[allow(clippy::too_many_arguments)] pub fn cast_shape_predicate( &self, @@ -411,9 +411,9 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::cast_shape`] - /// - [`SpatialQuery::cast_shape_predicate`] - /// - [`SpatialQuery::shape_hits_callback`] + /// - [`SpatialQueryPipeline::cast_shape`] + /// - [`SpatialQueryPipeline::cast_shape_predicate`] + /// - [`SpatialQueryPipeline::shape_hits_callback`] #[allow(clippy::too_many_arguments)] pub fn shape_hits( &self, @@ -447,9 +447,9 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::cast_shape`] - /// - [`SpatialQuery::cast_shape_predicate`] - /// - [`SpatialQuery::shape_hits`] + /// - [`SpatialQueryPipeline::cast_shape`] + /// - [`SpatialQueryPipeline::cast_shape_predicate`] + /// - [`SpatialQueryPipeline::shape_hits`] #[allow(clippy::too_many_arguments)] pub fn shape_hits_callback( &self, @@ -531,7 +531,7 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::project_point_predicate`] + /// - [`SpatialQueryPipeline::project_point_predicate`] pub fn project_point( &self, point: Vector, @@ -554,7 +554,7 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::project_point`] + /// - [`SpatialQueryPipeline::project_point`] pub fn project_point_predicate( &self, point: Vector, @@ -586,7 +586,7 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::point_intersections_callback`] + /// - [`SpatialQueryPipeline::point_intersections_callback`] pub fn point_intersections( &self, point: Vector, @@ -612,7 +612,7 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::point_intersections`] + /// - [`SpatialQueryPipeline::point_intersections`] pub fn point_intersections_callback( &self, point: Vector, @@ -642,7 +642,7 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::aabb_intersections_with_aabb_callback`] + /// - [`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| { @@ -658,7 +658,7 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::aabb_intersections_with_aabb`] + /// - [`SpatialQueryPipeline::aabb_intersections_with_aabb`] pub fn aabb_intersections_with_aabb_callback( &self, aabb: ColliderAabb, @@ -691,7 +691,7 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::shape_intersections_callback`] + /// - [`SpatialQueryPipeline::shape_intersections_callback`] pub fn shape_intersections( &self, shape: &Collider, @@ -727,7 +727,7 @@ impl SpatialQueryPipeline { /// /// ## Related Methods /// - /// - [`SpatialQuery::shape_intersections`] + /// - [`SpatialQueryPipeline::shape_intersections`] pub fn shape_intersections_callback( &self, shape: &Collider, From fb62066faf484b524149b15dd2d35b459ede8ec5 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sat, 7 Sep 2024 22:08:49 +0300 Subject: [PATCH 5/7] Fix missing argument in docs --- src/spatial_query/system_param.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/spatial_query/system_param.rs b/src/spatial_query/system_param.rs index fa37eb6c..ff4a508d 100644 --- a/src/spatial_query/system_param.rs +++ b/src/spatial_query/system_param.rs @@ -392,6 +392,7 @@ impl<'w, 's> SpatialQuery<'w, 's> { /// - `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. /// - `predicate`: A function called on each entity hit by the shape. The shape keeps travelling until the predicate returns `false`. /// /// ## Example From 43f328649d3a68bcc6ceb5cf40ddfb4a708cbf3a Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sat, 7 Sep 2024 23:43:53 +0300 Subject: [PATCH 6/7] Add constructors and helpers --- src/spatial_query/ray_caster.rs | 36 ++++++++++++++++++++++ src/spatial_query/shape_caster.rs | 51 +++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/src/spatial_query/ray_caster.rs b/src/spatial_query/ray_caster.rs index 2579ae29..c1d7baa4 100644 --- a/src/spatial_query/ray_caster.rs +++ b/src/spatial_query/ray_caster.rs @@ -394,6 +394,40 @@ impl Default for RayCastConfig { } } +impl RayCastConfig { + /// Creates a new [`RayCastConfig`] with a given maximum distance the ray can travel. + #[inline] + pub fn from_max_distance(max_distance: Scalar) -> Self { + Self { + max_distance, + ..default() + } + } + + /// Creates a new [`RayCastConfig`] with a given [`SpatialQueryFilter`]. + #[inline] + pub fn from_filter(filter: SpatialQueryFilter) -> Self { + Self { + filter, + ..default() + } + } + + /// Sets the maximum distance the ray can travel. + #[inline] + pub fn with_max_distance(mut self, max_distance: Scalar) -> Self { + self.max_distance = max_distance; + self + } + + /// Sets the [`SpatialQueryFilter`] for the ray cast. + #[inline] + pub fn with_filter(mut self, filter: SpatialQueryFilter) -> Self { + self.filter = filter; + self + } +} + /// Contains the hits of a ray cast by a [`RayCaster`]. /// /// The maximum number of hits depends on the value of `max_hits` in [`RayCaster`]. @@ -492,8 +526,10 @@ 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. 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 a71f15f3..8ac83201 100644 --- a/src/spatial_query/shape_caster.rs +++ b/src/spatial_query/shape_caster.rs @@ -473,6 +473,57 @@ impl Default for ShapeCastConfig { } } +impl ShapeCastConfig { + /// Creates a new [`ShapeCastConfig`] with a given maximum distance the shape can travel. + #[inline] + pub fn from_max_distance(max_distance: Scalar) -> Self { + Self { + max_distance, + ..default() + } + } + + /// Creates a new [`ShapeCastConfig`] with a given separation distance at which + /// the shapes will be considered as impacting. + #[inline] + pub fn from_target_distance(target_distance: Scalar) -> Self { + Self { + target_distance, + ..default() + } + } + + /// Creates a new [`ShapeCastConfig`] with a given [`SpatialQueryFilter`]. + #[inline] + pub fn from_filter(filter: SpatialQueryFilter) -> Self { + Self { + filter, + ..default() + } + } + + /// Sets the maximum distance the shape can travel. + #[inline] + pub 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 fn with_target_distance(mut self, target_distance: Scalar) -> Self { + self.target_distance = target_distance; + self + } + + /// Sets the [`SpatialQueryFilter`] for the shape cast. + #[inline] + pub fn with_filter(mut self, filter: SpatialQueryFilter) -> Self { + self.filter = filter; + 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 From edb26f85896de232ec4c0cc4791f7a14ff77cf21 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sun, 8 Sep 2024 00:58:09 +0300 Subject: [PATCH 7/7] Make methods const --- src/spatial_query/query_filter.rs | 16 +++++++---- src/spatial_query/ray_caster.rs | 24 ++++++++++------ src/spatial_query/shape_caster.rs | 47 +++++++++++++++++++------------ 3 files changed, 56 insertions(+), 31 deletions(-) diff --git a/src/spatial_query/query_filter.rs b/src/spatial_query/query_filter.rs index 3f9f0290..e6fad055 100644 --- a/src/spatial_query/query_filter.rs +++ b/src/spatial_query/query_filter.rs @@ -1,4 +1,7 @@ -use bevy::{prelude::*, utils::EntityHashSet}; +use bevy::{ + prelude::*, + utils::{EntityHash, EntityHashSet}, +}; use crate::prelude::*; @@ -41,14 +44,17 @@ pub struct SpatialQueryFilter { 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). pub fn from_mask(mask: impl Into) -> Self { diff --git a/src/spatial_query/ray_caster.rs b/src/spatial_query/ray_caster.rs index c1d7baa4..1c748b6b 100644 --- a/src/spatial_query/ray_caster.rs +++ b/src/spatial_query/ray_caster.rs @@ -395,36 +395,44 @@ impl Default for RayCastConfig { } impl RayCastConfig { + /// The default [`RayCastConfig`] configuration. + pub const DEFAULT: Self = Self { + max_distance: Scalar::MAX, + solid: true, + filter: SpatialQueryFilter::DEFAULT, + }; + /// Creates a new [`RayCastConfig`] with a given maximum distance the ray can travel. #[inline] - pub fn from_max_distance(max_distance: Scalar) -> Self { + pub const fn from_max_distance(max_distance: Scalar) -> Self { Self { max_distance, - ..default() + solid: true, + filter: SpatialQueryFilter::DEFAULT, } } /// Creates a new [`RayCastConfig`] with a given [`SpatialQueryFilter`]. #[inline] - pub fn from_filter(filter: SpatialQueryFilter) -> Self { + pub const fn from_filter(filter: SpatialQueryFilter) -> Self { Self { + max_distance: Scalar::MAX, + solid: true, filter, - ..default() } } /// Sets the maximum distance the ray can travel. #[inline] - pub fn with_max_distance(mut self, max_distance: Scalar) -> Self { + pub const fn with_max_distance(mut self, max_distance: Scalar) -> Self { self.max_distance = max_distance; self } /// Sets the [`SpatialQueryFilter`] for the ray cast. #[inline] - pub fn with_filter(mut self, filter: SpatialQueryFilter) -> Self { - self.filter = filter; - self + pub const fn with_filter(&self, filter: SpatialQueryFilter) -> Self { + Self { filter, ..*self } } } diff --git a/src/spatial_query/shape_caster.rs b/src/spatial_query/shape_caster.rs index 8ac83201..1d202f95 100644 --- a/src/spatial_query/shape_caster.rs +++ b/src/spatial_query/shape_caster.rs @@ -463,64 +463,75 @@ pub struct ShapeCastConfig { impl Default for ShapeCastConfig { fn default() -> Self { - Self { - max_distance: Scalar::MAX, - target_distance: 0.0, - compute_impact_on_penetration: true, - ignore_origin_penetration: false, - filter: SpatialQueryFilter::default(), - } + Self::DEFAULT } } impl ShapeCastConfig { + /// The default [`ShapeCastConfig`] configuration. + pub const DEFAULT: Self = Self { + max_distance: Scalar::MAX, + target_distance: 0.0, + compute_impact_on_penetration: true, + ignore_origin_penetration: false, + filter: SpatialQueryFilter::DEFAULT, + }; + /// Creates a new [`ShapeCastConfig`] with a given maximum distance the shape can travel. #[inline] - pub fn from_max_distance(max_distance: Scalar) -> Self { + pub const fn from_max_distance(max_distance: Scalar) -> Self { Self { max_distance, - ..default() + target_distance: 0.0, + compute_impact_on_penetration: true, + ignore_origin_penetration: false, + filter: SpatialQueryFilter::DEFAULT, } } /// Creates a new [`ShapeCastConfig`] with a given separation distance at which /// the shapes will be considered as impacting. #[inline] - pub fn from_target_distance(target_distance: Scalar) -> Self { + pub const fn from_target_distance(target_distance: Scalar) -> Self { Self { + max_distance: Scalar::MAX, target_distance, - ..default() + compute_impact_on_penetration: true, + ignore_origin_penetration: false, + filter: SpatialQueryFilter::DEFAULT, } } /// Creates a new [`ShapeCastConfig`] with a given [`SpatialQueryFilter`]. #[inline] - pub fn from_filter(filter: SpatialQueryFilter) -> Self { + pub const fn from_filter(filter: SpatialQueryFilter) -> Self { Self { + max_distance: Scalar::MAX, + target_distance: 0.0, + compute_impact_on_penetration: true, + ignore_origin_penetration: false, filter, - ..default() } } /// Sets the maximum distance the shape can travel. #[inline] - pub fn with_max_distance(mut self, max_distance: Scalar) -> Self { + 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 fn with_target_distance(mut self, target_distance: Scalar) -> Self { + pub const fn with_target_distance(mut self, target_distance: Scalar) -> Self { self.target_distance = target_distance; self } /// Sets the [`SpatialQueryFilter`] for the shape cast. #[inline] - pub fn with_filter(mut self, filter: SpatialQueryFilter) -> Self { - self.filter = filter; - self + pub const fn with_filter(&self, filter: SpatialQueryFilter) -> Self { + Self { filter, ..*self } } }