1use crate::directional_navigation::{AutoNavigationConfig, FocusableArea};
3use bevy_ecs::prelude::*;
4use bevy_math::{CompassOctant, Dir2, Rect, Vec2};
5
6fn calculate_1d_overlap(
13 origin_pos: f32,
14 origin_size: f32,
15 candidate_pos: f32,
16 candidate_size: f32,
17) -> f32 {
18 let origin_min = origin_pos - origin_size / 2.0;
19 let origin_max = origin_pos + origin_size / 2.0;
20 let cand_min = candidate_pos - candidate_size / 2.0;
21 let cand_max = candidate_pos + candidate_size / 2.0;
22
23 let overlap = (origin_max.min(cand_max) - origin_min.max(cand_min)).max(0.0);
24 let max_overlap = origin_size.min(candidate_size);
25 if max_overlap > 0.0 {
26 overlap / max_overlap
27 } else {
28 0.0
29 }
30}
31
32fn calculate_overlap(
37 origin_pos: Vec2,
38 origin_size: Vec2,
39 candidate_pos: Vec2,
40 candidate_size: Vec2,
41 octant: CompassOctant,
42) -> f32 {
43 match octant {
44 CompassOctant::North | CompassOctant::South => {
45 calculate_1d_overlap(
47 origin_pos.x,
48 origin_size.x,
49 candidate_pos.x,
50 candidate_size.x,
51 )
52 }
53 CompassOctant::East | CompassOctant::West => {
54 calculate_1d_overlap(
56 origin_pos.y,
57 origin_size.y,
58 candidate_pos.y,
59 candidate_size.y,
60 )
61 }
62 _ => 1.0,
64 }
65}
66
67fn score_candidate(
71 origin_pos: Vec2,
72 origin_size: Vec2,
73 candidate_pos: Vec2,
74 candidate_size: Vec2,
75 octant: CompassOctant,
76 config: &AutoNavigationConfig,
77) -> f32 {
78 let dir = Dir2::from(octant).as_vec2() * Vec2::new(1.0, -1.0);
80 let to_candidate = candidate_pos - origin_pos;
81
82 let origin_math = Vec2::new(origin_pos.x, -origin_pos.y);
85 let candidate_math = Vec2::new(candidate_pos.x, -candidate_pos.y);
86 if !octant.is_in_direction(origin_math, candidate_math) {
87 return f32::INFINITY;
88 }
89
90 let overlap_factor = calculate_overlap(
92 origin_pos,
93 origin_size,
94 candidate_pos,
95 candidate_size,
96 octant,
97 );
98
99 if overlap_factor < config.min_alignment_factor {
100 return f32::INFINITY;
101 }
102
103 let origin_rect = Rect::from_center_size(origin_pos, origin_size);
105 let candidate_rect = Rect::from_center_size(candidate_pos, candidate_size);
106 let dx = (candidate_rect.min.x - origin_rect.max.x)
107 .max(origin_rect.min.x - candidate_rect.max.x)
108 .max(0.0);
109 let dy = (candidate_rect.min.y - origin_rect.max.y)
110 .max(origin_rect.min.y - candidate_rect.max.y)
111 .max(0.0);
112 let distance = (dx * dx + dy * dy).sqrt();
113
114 if let Some(max_dist) = config.max_search_distance {
116 if distance > max_dist {
117 return f32::INFINITY;
118 }
119 }
120
121 let center_distance = to_candidate.length();
123 let alignment = if center_distance > 0.0 {
124 to_candidate.normalize().dot(dir).max(0.0)
125 } else {
126 1.0
127 };
128
129 let alignment_penalty = if config.prefer_aligned {
132 (1.0 - alignment) * distance * 2.0 } else {
134 0.0
135 };
136
137 distance + alignment_penalty
138}
139
140pub fn find_best_candidate(
145 origin: &FocusableArea,
146 direction: CompassOctant,
147 candidates: &[FocusableArea],
148 config: &AutoNavigationConfig,
149) -> Option<Entity> {
150 let mut best_candidate = None;
152 let mut best_score = f32::INFINITY;
153
154 for candidate in candidates {
155 if candidate.entity == origin.entity {
157 continue;
158 }
159
160 let score = score_candidate(
162 origin.position,
163 origin.size,
164 candidate.position,
165 candidate.size,
166 direction,
167 config,
168 );
169
170 if score < best_score {
171 best_score = score;
172 best_candidate = Some(candidate.entity);
173 }
174 }
175
176 best_candidate
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn test_is_in_direction() {
185 let origin = Vec2::new(100.0, 100.0);
186
187 let north_node = Vec2::new(100.0, 150.0);
189 assert!(CompassOctant::North.is_in_direction(origin, north_node));
190 assert!(!CompassOctant::South.is_in_direction(origin, north_node));
191
192 let south_node = Vec2::new(100.0, 50.0);
194 assert!(CompassOctant::South.is_in_direction(origin, south_node));
195 assert!(!CompassOctant::North.is_in_direction(origin, south_node));
196
197 let east_node = Vec2::new(150.0, 100.0);
199 assert!(CompassOctant::East.is_in_direction(origin, east_node));
200 assert!(!CompassOctant::West.is_in_direction(origin, east_node));
201
202 let ne_node = Vec2::new(150.0, 150.0);
204 assert!(CompassOctant::NorthEast.is_in_direction(origin, ne_node));
205 assert!(!CompassOctant::SouthWest.is_in_direction(origin, ne_node));
206 }
207
208 #[test]
209 fn test_calculate_overlap_horizontal() {
210 let origin_pos = Vec2::new(100.0, 100.0);
211 let origin_size = Vec2::new(50.0, 50.0);
212
213 let north_pos = Vec2::new(100.0, 200.0);
215 let north_size = Vec2::new(50.0, 50.0);
216 let overlap = calculate_overlap(
217 origin_pos,
218 origin_size,
219 north_pos,
220 north_size,
221 CompassOctant::North,
222 );
223 assert_eq!(overlap, 1.0); let north_pos = Vec2::new(110.0, 200.0);
227 let partial_overlap = calculate_overlap(
228 origin_pos,
229 origin_size,
230 north_pos,
231 north_size,
232 CompassOctant::North,
233 );
234 assert!(partial_overlap > 0.0 && partial_overlap < 1.0);
235
236 let north_pos = Vec2::new(200.0, 200.0);
238 let no_overlap = calculate_overlap(
239 origin_pos,
240 origin_size,
241 north_pos,
242 north_size,
243 CompassOctant::North,
244 );
245 assert_eq!(no_overlap, 0.0);
246 }
247
248 #[test]
249 fn test_score_candidate() {
250 let config = AutoNavigationConfig::default();
251 let origin_pos = Vec2::new(100.0, 100.0);
252 let origin_size = Vec2::new(50.0, 50.0);
253
254 let north_pos = Vec2::new(100.0, 0.0);
256 let north_size = Vec2::new(50.0, 50.0);
257 let north_score = score_candidate(
258 origin_pos,
259 origin_size,
260 north_pos,
261 north_size,
262 CompassOctant::North,
263 &config,
264 );
265 assert!(north_score < f32::INFINITY);
266 assert!(north_score < 150.0); let south_pos = Vec2::new(100.0, 200.0);
270 let south_size = Vec2::new(50.0, 50.0);
271 let invalid_score = score_candidate(
272 origin_pos,
273 origin_size,
274 south_pos,
275 south_size,
276 CompassOctant::North,
277 &config,
278 );
279 assert_eq!(invalid_score, f32::INFINITY);
280
281 let close_pos = Vec2::new(100.0, 50.0);
283 let far_pos = Vec2::new(100.0, -100.0);
284 let close_score = score_candidate(
285 origin_pos,
286 origin_size,
287 close_pos,
288 north_size,
289 CompassOctant::North,
290 &config,
291 );
292 let far_score = score_candidate(
293 origin_pos,
294 origin_size,
295 far_pos,
296 north_size,
297 CompassOctant::North,
298 &config,
299 );
300 assert!(close_score < far_score);
301 }
302}