1use std::cell::OnceCell;
2
3use crate::math::{AdjustPrecision, AsF32, Float, Vector3};
4use bevy::{math::InvalidDirectionError, prelude::*};
5use bevy_tnua_physics_integration_layer::{
6 obstacle_radar::TnuaObstacleRadar,
7 spatial_ext::{TnuaPointProjectionResult, TnuaSpatialExt},
8};
9
10pub struct TnuaRadarLens<'a, X: TnuaSpatialExt> {
12 radar: &'a TnuaObstacleRadar,
13 ext: &'a X,
14}
15
16impl<'a, X: TnuaSpatialExt> TnuaRadarLens<'a, X> {
17 pub fn new(radar: &'a TnuaObstacleRadar, ext: &'a X) -> Self {
24 Self { radar, ext }
25 }
26
27 pub fn iter_blips(&'_ self) -> impl Iterator<Item = TnuaRadarBlipLens<'_, X>> {
30 self.radar.iter_blips().filter_map(|entity| {
31 Some(TnuaRadarBlipLens {
32 radar_lens: self,
33 entity,
34 collider_data: self.ext.fetch_collider_data(entity)?,
35 closest_point_cache: OnceCell::new(),
36 closest_point_normal_cache: OnceCell::new(),
37 })
38 })
39 }
40}
41
42pub struct TnuaRadarBlipLens<'a, X: TnuaSpatialExt> {
43 radar_lens: &'a TnuaRadarLens<'a, X>,
44 entity: Entity,
45 pub collider_data: X::ColliderData<'a>,
50 closest_point_cache: OnceCell<TnuaPointProjectionResult>,
51 closest_point_normal_cache: OnceCell<Vector3>,
52}
53
54impl<X: TnuaSpatialExt> TnuaRadarBlipLens<'_, X> {
55 fn radar(&self) -> &TnuaObstacleRadar {
56 self.radar_lens.radar
57 }
58
59 pub fn entity(&self) -> Entity {
61 self.entity
62 }
63
64 pub fn is_interactable(&self) -> bool {
67 self.radar_lens
68 .ext
69 .can_interact(self.radar().tracked_entity(), self.entity)
70 }
71
72 pub fn closest_point(&self) -> TnuaPointProjectionResult {
75 *self.closest_point_cache.get_or_init(|| {
76 self.radar_lens.ext.project_point(
77 self.radar().tracked_position(),
78 false,
79 &self.collider_data,
80 )
81 })
82 }
83
84 pub fn closest_point_from(&self, point: Vector3, solid: bool) -> TnuaPointProjectionResult {
87 self.radar_lens
88 .ext
89 .project_point(point, solid, &self.collider_data)
90 }
91
92 pub fn closest_point_from_offset(
95 &self,
96 offset: Vector3,
97 solid: bool,
98 ) -> TnuaPointProjectionResult {
99 self.closest_point_from(self.radar().tracked_position() + offset, solid)
100 }
101
102 pub fn flat_wall_score(&self, up: Dir3, offsets: &[Float]) -> Float {
105 let Some(closest_point) = self.closest_point().outside() else {
106 return 0.0;
107 };
108 1.0 - offsets
109 .iter()
110 .map(|offset| {
111 if *offset == 0.0 {
112 return 0.0;
113 }
114 let offset_vec = *offset * up.adjust_precision();
115 let expected = closest_point + offset_vec;
116 let actual = self.closest_point_from_offset(offset_vec, false).get();
117 let dist = expected.distance_squared(actual);
118 dist / offset.powi(2)
119 })
120 .sum::<Float>()
121 / offsets.len() as Float
122 }
123
124 pub fn probe_extent_from_closest_point(
141 &self,
142 direction: Dir3,
143 probe_at_distance: Float,
144 ) -> Float {
145 let closest_point = self.closest_point().get();
146 let closest_above = self
147 .closest_point_from_offset(probe_at_distance * direction.adjust_precision(), false)
148 .get();
149 (closest_above - closest_point).dot(direction.adjust_precision())
150 }
151
152 pub fn direction_to_closest_point(&self) -> Result<Dir3, InvalidDirectionError> {
158 match self.closest_point() {
159 TnuaPointProjectionResult::Outside(closest_point) => {
160 Dir3::new((closest_point - self.radar().tracked_position()).f32())
161 }
162 TnuaPointProjectionResult::Inside(closest_point) => {
163 Dir3::new((self.radar().tracked_position() - closest_point).f32())
164 }
165 }
166 }
167
168 pub fn normal_from_closest_point(&self) -> Vector3 {
171 *self.closest_point_normal_cache.get_or_init(|| {
172 let origin = self.radar().tracked_position();
173
174 let get_normal = |closest_point: Vector3| -> Vector3 {
175 let Some(direction) = (closest_point - origin).try_normalize() else {
176 return Vector3::ZERO;
177 };
178 let Some((_, normal)) = self.radar_lens.ext.cast_ray(
179 origin,
180 direction,
181 Float::INFINITY,
182 &self.collider_data,
183 ) else {
184 warn!("Unable to query normal to already-found closest point");
185 return Vector3::ZERO;
186 };
187 normal
188 };
189
190 match self.closest_point() {
191 TnuaPointProjectionResult::Outside(closest_point) => get_normal(closest_point),
192 TnuaPointProjectionResult::Inside(closest_point) => -get_normal(closest_point),
193 }
194 })
195 }
196
197 pub fn spatial_relation(&self, threshold: Float) -> TnuaBlipSpatialRelation {
199 let Ok(direction) = self.direction_to_closest_point() else {
200 return TnuaBlipSpatialRelation::Invalid;
201 };
202 let dot_up = self
203 .radar()
204 .up_direction()
205 .dot(*direction)
206 .adjust_precision();
207 if threshold < dot_up {
208 TnuaBlipSpatialRelation::Above
209 } else if dot_up < -threshold {
210 TnuaBlipSpatialRelation::Below
211 } else {
212 let planar_direction =
213 Dir3::new(direction.reject_from_normalized(*self.radar().up_direction()))
214 .expect("since the dot-up is smaller than the threshold, the direction should not be parallel with the direction");
215 TnuaBlipSpatialRelation::Aeside(planar_direction)
216 }
217 }
218}
219
220#[derive(Debug)]
222pub enum TnuaBlipSpatialRelation {
223 Invalid,
224 Above,
225 Below,
226 Aeside(Dir3),
227}