use crate::directional_navigation::{AutoNavigationConfig, FocusableArea};
use bevy_ecs::prelude::*;
use bevy_math::{CompassOctant, Dir2, Rect, Vec2};
fn calculate_1d_overlap(
origin_pos: f32,
origin_size: f32,
candidate_pos: f32,
candidate_size: f32,
) -> f32 {
let origin_min = origin_pos - origin_size / 2.0;
let origin_max = origin_pos + origin_size / 2.0;
let cand_min = candidate_pos - candidate_size / 2.0;
let cand_max = candidate_pos + candidate_size / 2.0;
let overlap = (origin_max.min(cand_max) - origin_min.max(cand_min)).max(0.0);
let max_overlap = origin_size.min(candidate_size);
if max_overlap > 0.0 {
overlap / max_overlap
} else {
0.0
}
}
fn calculate_overlap(
origin_pos: Vec2,
origin_size: Vec2,
candidate_pos: Vec2,
candidate_size: Vec2,
octant: CompassOctant,
) -> f32 {
match octant {
CompassOctant::North | CompassOctant::South => {
calculate_1d_overlap(
origin_pos.x,
origin_size.x,
candidate_pos.x,
candidate_size.x,
)
}
CompassOctant::East | CompassOctant::West => {
calculate_1d_overlap(
origin_pos.y,
origin_size.y,
candidate_pos.y,
candidate_size.y,
)
}
_ => 1.0,
}
}
fn score_candidate(
origin_pos: Vec2,
origin_size: Vec2,
candidate_pos: Vec2,
candidate_size: Vec2,
octant: CompassOctant,
config: &AutoNavigationConfig,
) -> f32 {
let dir = Dir2::from(octant).as_vec2() * Vec2::new(1.0, -1.0);
let to_candidate = candidate_pos - origin_pos;
let origin_math = Vec2::new(origin_pos.x, -origin_pos.y);
let candidate_math = Vec2::new(candidate_pos.x, -candidate_pos.y);
if !octant.is_in_direction(origin_math, candidate_math) {
return f32::INFINITY;
}
let overlap_factor = calculate_overlap(
origin_pos,
origin_size,
candidate_pos,
candidate_size,
octant,
);
if overlap_factor < config.min_alignment_factor {
return f32::INFINITY;
}
let origin_rect = Rect::from_center_size(origin_pos, origin_size);
let candidate_rect = Rect::from_center_size(candidate_pos, candidate_size);
let dx = (candidate_rect.min.x - origin_rect.max.x)
.max(origin_rect.min.x - candidate_rect.max.x)
.max(0.0);
let dy = (candidate_rect.min.y - origin_rect.max.y)
.max(origin_rect.min.y - candidate_rect.max.y)
.max(0.0);
let distance = (dx * dx + dy * dy).sqrt();
if let Some(max_dist) = config.max_search_distance {
if distance > max_dist {
return f32::INFINITY;
}
}
let center_distance = to_candidate.length();
let alignment = if center_distance > 0.0 {
to_candidate.normalize().dot(dir).max(0.0)
} else {
1.0
};
let alignment_penalty = if config.prefer_aligned {
(1.0 - alignment) * distance * 2.0 } else {
0.0
};
distance + alignment_penalty
}
pub fn find_best_candidate(
origin: &FocusableArea,
direction: CompassOctant,
candidates: &[FocusableArea],
config: &AutoNavigationConfig,
) -> Option<Entity> {
let mut best_candidate = None;
let mut best_score = f32::INFINITY;
for candidate in candidates {
if candidate.entity == origin.entity {
continue;
}
let score = score_candidate(
origin.position,
origin.size,
candidate.position,
candidate.size,
direction,
config,
);
if score < best_score {
best_score = score;
best_candidate = Some(candidate.entity);
}
}
best_candidate
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_in_direction() {
let origin = Vec2::new(100.0, 100.0);
let north_node = Vec2::new(100.0, 150.0);
assert!(CompassOctant::North.is_in_direction(origin, north_node));
assert!(!CompassOctant::South.is_in_direction(origin, north_node));
let south_node = Vec2::new(100.0, 50.0);
assert!(CompassOctant::South.is_in_direction(origin, south_node));
assert!(!CompassOctant::North.is_in_direction(origin, south_node));
let east_node = Vec2::new(150.0, 100.0);
assert!(CompassOctant::East.is_in_direction(origin, east_node));
assert!(!CompassOctant::West.is_in_direction(origin, east_node));
let ne_node = Vec2::new(150.0, 150.0);
assert!(CompassOctant::NorthEast.is_in_direction(origin, ne_node));
assert!(!CompassOctant::SouthWest.is_in_direction(origin, ne_node));
}
#[test]
fn test_calculate_overlap_horizontal() {
let origin_pos = Vec2::new(100.0, 100.0);
let origin_size = Vec2::new(50.0, 50.0);
let north_pos = Vec2::new(100.0, 200.0);
let north_size = Vec2::new(50.0, 50.0);
let overlap = calculate_overlap(
origin_pos,
origin_size,
north_pos,
north_size,
CompassOctant::North,
);
assert_eq!(overlap, 1.0);
let north_pos = Vec2::new(110.0, 200.0);
let partial_overlap = calculate_overlap(
origin_pos,
origin_size,
north_pos,
north_size,
CompassOctant::North,
);
assert!(partial_overlap > 0.0 && partial_overlap < 1.0);
let north_pos = Vec2::new(200.0, 200.0);
let no_overlap = calculate_overlap(
origin_pos,
origin_size,
north_pos,
north_size,
CompassOctant::North,
);
assert_eq!(no_overlap, 0.0);
}
#[test]
fn test_score_candidate() {
let config = AutoNavigationConfig::default();
let origin_pos = Vec2::new(100.0, 100.0);
let origin_size = Vec2::new(50.0, 50.0);
let north_pos = Vec2::new(100.0, 0.0);
let north_size = Vec2::new(50.0, 50.0);
let north_score = score_candidate(
origin_pos,
origin_size,
north_pos,
north_size,
CompassOctant::North,
&config,
);
assert!(north_score < f32::INFINITY);
assert!(north_score < 150.0);
let south_pos = Vec2::new(100.0, 200.0);
let south_size = Vec2::new(50.0, 50.0);
let invalid_score = score_candidate(
origin_pos,
origin_size,
south_pos,
south_size,
CompassOctant::North,
&config,
);
assert_eq!(invalid_score, f32::INFINITY);
let close_pos = Vec2::new(100.0, 50.0);
let far_pos = Vec2::new(100.0, -100.0);
let close_score = score_candidate(
origin_pos,
origin_size,
close_pos,
north_size,
CompassOctant::North,
&config,
);
let far_score = score_candidate(
origin_pos,
origin_size,
far_pos,
north_size,
CompassOctant::North,
&config,
);
assert!(close_score < far_score);
}
}