Path: blob/main/crates/bevy_input_focus/src/directional_navigation.rs
9366 views
//! A manual navigation framework for moving between focusable elements based on directional input.1//!2//! Note: If using `bevy_ui`, this manual navigation framework is used to provide overrides3//! for its automatic navigation framework based on the `AutoDirectionalNavigation` component.4//! Most times, the automatic navigation framework alone should be sufficient.5//! If not using `bevy_ui`, this manual navigation framework can still be used by itself.6//!7//! While virtual cursors are a common way to navigate UIs with a gamepad (or arrow keys!),8//! they are generally both slow and frustrating to use.9//! Instead, directional inputs should provide a direct way to snap between focusable elements.10//!11//! Like the rest of this crate, the [`InputFocus`] resource is manipulated to track12//! the current focus.13//!14//! This module's [`DirectionalNavigationMap`] stores a directed graph of focusable entities.15//! Each entity can have up to 8 neighbors, one for each [`CompassOctant`], balancing16//! flexibility and required precision.17//!18//! Navigating between focusable entities (commonly UI nodes) is done by19//! passing a [`CompassOctant`] into the [`navigate`](DirectionalNavigation::navigate) method20//! from the [`DirectionalNavigation`] system parameter. Under the hood, the21//! [`DirectionalNavigationMap`] is used to return the focusable entity in a direction22//! for a given entity.23//!24//! # Setting up Directional Navigation25//!26//! ## Automatic Navigation (Recommended)27//!28//! The easiest way to set up navigation is to add the `AutoDirectionalNavigation` component29//! to your UI entities. This component is available in the `bevy_ui` crate. If you choose to30//! include automatic navigation, you should also use the `AutoDirectionalNavigator` system parameter31//! in that crate instead of [`DirectionalNavigation`].32//!33//! ## Combining Automatic Navigation with Manual Overrides34//!35//! Following manual edges always take precedence, allowing you to use36//! automatic navigation for most UI elements while overriding specific connections for37//! special cases like wrapping menus or cross-layer navigation. If you need to override38//! automatic navigation behavior, use the [`DirectionalNavigationMap`] to define39//! overriding edges between UI entities.40//!41//! ## Manual Navigation Only42//!43//! Manually define your navigation using the [`DirectionalNavigationMap`], and use the44//! [`DirectionalNavigation`] system parameter to navigate between components.45//! You can define navigation connections using methods like46//! [`add_edge`](DirectionalNavigationMap::add_edge) and47//! [`add_looping_edges`](DirectionalNavigationMap::add_looping_edges).48//!49//! ## When to Use Manual Navigation or Manual Overrides50//!51//! While automatic navigation is recommended and satisfactory for most use cases,52//! using manual navigation only or integrating manual overrides to automatic navigation provide:53//!54//! - **Precise control**: Define exact navigation flow, including non-obvious connections like looping edges55//! - **Cross-layer navigation**: Connect elements across different UI layers or z-index levels56//! - **Custom behavior**: Implement domain-specific navigation patterns (e.g., spreadsheet-style wrapping)5758use crate::{navigator::find_best_candidate, InputFocus};59use bevy_app::prelude::*;60use bevy_ecs::{61entity::{EntityHashMap, EntityHashSet},62prelude::*,63system::SystemParam,64};65use bevy_math::{CompassOctant, Vec2};66use thiserror::Error;6768#[cfg(feature = "bevy_reflect")]69use bevy_reflect::{prelude::*, Reflect};7071/// A plugin that sets up the directional navigation resources.72#[derive(Default)]73pub struct DirectionalNavigationPlugin;7475impl Plugin for DirectionalNavigationPlugin {76fn build(&self, app: &mut App) {77app.init_resource::<DirectionalNavigationMap>()78.init_resource::<AutoNavigationConfig>();79}80}8182/// Configuration resource for automatic directional navigation and for generating manual83/// navigation edges via [`auto_generate_navigation_edges`]84///85/// This resource controls how nodes should be automatically connected in each direction.86#[derive(Resource, Debug, Clone, PartialEq)]87#[cfg_attr(88feature = "bevy_reflect",89derive(Reflect),90reflect(Resource, Debug, PartialEq, Clone)91)]92pub struct AutoNavigationConfig {93/// Minimum overlap ratio (0.0-1.0) required along the perpendicular axis for cardinal directions.94///95/// This parameter controls how much two UI elements must overlap in the perpendicular direction96/// to be considered reachable neighbors. It only applies to cardinal directions (`North`, `South`, `East`, `West`);97/// diagonal directions (`NorthEast`, `SouthEast`, etc.) ignore this requirement entirely.98///99/// # Calculation100///101/// The overlap factor is calculated as:102/// ```text103/// overlap_factor = actual_overlap / min(origin_size, candidate_size)104/// ```105///106/// For East/West navigation, this measures vertical overlap:107/// - `actual_overlap` = overlapping height between the two elements108/// - Sizes are the heights of the origin and candidate109///110/// For North/South navigation, this measures horizontal overlap:111/// - `actual_overlap` = overlapping width between the two elements112/// - Sizes are the widths of the origin and candidate113///114/// # Examples115///116/// - `0.0` (default): Any overlap is sufficient. Even if elements barely touch, they can be neighbors.117/// - `0.5`: Elements must overlap by at least 50% of the smaller element's size.118/// - `1.0`: Perfect alignment required. The smaller element must be completely within the bounds119/// of the larger element along the perpendicular axis.120///121/// # Use Cases122///123/// - **Sparse/irregular layouts** (e.g., star constellations): Use `0.0` to allow navigation124/// between elements that don't directly align.125/// - **Grid layouts**: Use `0.5` or higher to ensure navigation only connects elements in126/// the same row or column.127/// - **Strict alignment**: Use `1.0` to require perfect alignment, though this may result128/// in disconnected navigation graphs if elements aren't precisely aligned.129pub min_alignment_factor: f32,130131/// Maximum search distance in logical pixels.132///133/// Nodes beyond this distance won't be connected. `None` means unlimited.134/// The distance between two UI elements is calculated using their closest edges.135pub max_search_distance: Option<f32>,136137/// Whether to prefer nodes that are more aligned with the exact direction.138///139/// When `true`, nodes that are more directly in line with the requested direction140/// will be strongly preferred over nodes at an angle.141pub prefer_aligned: bool,142}143144impl Default for AutoNavigationConfig {145fn default() -> Self {146Self {147min_alignment_factor: 0.0, // Any overlap is acceptable148max_search_distance: None, // No distance limit149prefer_aligned: true, // Prefer well-aligned nodes150}151}152}153154/// The up-to-eight neighbors of a focusable entity, one for each [`CompassOctant`].155#[derive(Default, Debug, Clone, PartialEq)]156#[cfg_attr(157feature = "bevy_reflect",158derive(Reflect),159reflect(Default, Debug, PartialEq, Clone)160)]161pub struct NavNeighbors {162/// The array of neighbors, one for each [`CompassOctant`].163/// The mapping between array elements and directions is determined by [`CompassOctant::to_index`].164///165/// If no neighbor exists in a given direction, the value will be [`None`].166/// In most cases, using [`NavNeighbors::set`] and [`NavNeighbors::get`]167/// will be more ergonomic than directly accessing this array.168pub neighbors: [Option<Entity>; 8],169}170171impl NavNeighbors {172/// An empty set of neighbors.173pub const EMPTY: NavNeighbors = NavNeighbors {174neighbors: [None; 8],175};176177/// Get the neighbor for a given [`CompassOctant`].178pub const fn get(&self, octant: CompassOctant) -> Option<Entity> {179self.neighbors[octant.to_index()]180}181182/// Set the neighbor for a given [`CompassOctant`].183pub const fn set(&mut self, octant: CompassOctant, entity: Entity) {184self.neighbors[octant.to_index()] = Some(entity);185}186}187188/// A resource that stores the manually specified traversable graph of focusable entities.189///190/// Each entity can have up to 8 neighbors, one for each [`CompassOctant`].191///192/// To ensure that your graph is intuitive to navigate and generally works correctly, it should be:193///194/// - **Connected**: Every focusable entity should be reachable from every other focusable entity.195/// - **Symmetric**: If entity A is a neighbor of entity B, then entity B should be a neighbor of entity A, ideally in the reverse direction.196/// - **Physical**: The direction of navigation should match the layout of the entities when possible,197/// although looping around the edges of the screen is also acceptable.198/// - **Not self-connected**: An entity should not be a neighbor of itself; use [`None`] instead.199///200/// This graph must be built and maintained manually, and the developer is responsible for ensuring that it meets the above criteria.201/// Notably, if the developer adds or removes the navigability of an entity, the developer should update the map as necessary.202///203/// If the automatic navigation system in `bevy_ui` is being used, this resource can be used to specify204/// manual navigation overrides. Any navigation edges specified in this map take precedence over automatic205/// navigation. For example, if navigation on one side of the window should wrap around to206/// the other side of the window, this navigation behavior can be specified using this map.207#[derive(Resource, Debug, Default, Clone, PartialEq)]208#[cfg_attr(209feature = "bevy_reflect",210derive(Reflect),211reflect(Resource, Debug, Default, PartialEq, Clone)212)]213pub struct DirectionalNavigationMap {214/// A directed graph of focusable entities.215///216/// Pass in the current focus as a key, and get back a collection of up to 8 neighbors,217/// each keyed by a [`CompassOctant`].218pub neighbors: EntityHashMap<NavNeighbors>,219}220221impl DirectionalNavigationMap {222/// Removes an entity from the navigation map, including all connections to and from it.223///224/// Note that this is an O(n) operation, where n is the number of entities in the map,225/// as we must iterate over each entity to check for connections to the removed entity.226///227/// If you are removing multiple entities, consider using [`remove_multiple`](Self::remove_multiple) instead.228pub fn remove(&mut self, entity: Entity) {229self.neighbors.remove(&entity);230231for node in self.neighbors.values_mut() {232for neighbor in node.neighbors.iter_mut() {233if *neighbor == Some(entity) {234*neighbor = None;235}236}237}238}239240/// Removes a collection of entities from the navigation map.241///242/// While this is still an O(n) operation, where n is the number of entities in the map,243/// it is more efficient than calling [`remove`](Self::remove) multiple times,244/// as we can check for connections to all removed entities in a single pass.245///246/// An [`EntityHashSet`] must be provided as it is noticeably faster than the standard hasher or a [`Vec`](`alloc::vec::Vec`).247pub fn remove_multiple(&mut self, entities: EntityHashSet) {248for entity in &entities {249self.neighbors.remove(entity);250}251252for node in self.neighbors.values_mut() {253for neighbor in node.neighbors.iter_mut() {254if let Some(entity) = *neighbor {255if entities.contains(&entity) {256*neighbor = None;257}258}259}260}261}262263/// Completely clears the navigation map, removing all entities and connections.264pub fn clear(&mut self) {265self.neighbors.clear();266}267268/// Adds an edge between two entities in the navigation map.269/// Any existing edge from A in the provided direction will be overwritten.270///271/// The reverse edge will not be added, so navigation will only be possible in one direction.272/// If you want to add a symmetrical edge, use [`add_symmetrical_edge`](Self::add_symmetrical_edge) instead.273pub fn add_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {274self.neighbors275.entry(a)276.or_insert(NavNeighbors::EMPTY)277.set(direction, b);278}279280/// Adds a symmetrical edge between two entities in the navigation map.281/// The A -> B path will use the provided direction, while B -> A will use the [`CompassOctant::opposite`] variant.282///283/// Any existing connections between the two entities will be overwritten.284pub fn add_symmetrical_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {285self.add_edge(a, b, direction);286self.add_edge(b, a, direction.opposite());287}288289/// Add symmetrical edges between each consecutive pair of entities in the provided slice.290///291/// Unlike [`add_looping_edges`](Self::add_looping_edges), this method does not loop back to the first entity.292pub fn add_edges(&mut self, entities: &[Entity], direction: CompassOctant) {293for pair in entities.windows(2) {294self.add_symmetrical_edge(pair[0], pair[1], direction);295}296}297298/// Add symmetrical edges between each consecutive pair of entities in the provided slice, looping back to the first entity at the end.299///300/// This is useful for creating a circular navigation path between a set of entities, such as a menu.301pub fn add_looping_edges(&mut self, entities: &[Entity], direction: CompassOctant) {302self.add_edges(entities, direction);303if let Some((first_entity, rest)) = entities.split_first() {304if let Some(last_entity) = rest.last() {305self.add_symmetrical_edge(*last_entity, *first_entity, direction);306}307}308}309310/// Gets the entity in a given direction from the current focus, if any.311pub fn get_neighbor(&self, focus: Entity, octant: CompassOctant) -> Option<Entity> {312self.neighbors313.get(&focus)314.and_then(|neighbors| neighbors.get(octant))315}316317/// Looks up the neighbors of a given entity.318///319/// If the entity is not in the map, [`None`] will be returned.320/// Note that the set of neighbors is not guaranteed to be non-empty though!321pub fn get_neighbors(&self, entity: Entity) -> Option<&NavNeighbors> {322self.neighbors.get(&entity)323}324}325326/// A system parameter for navigating between focusable entities in a directional way.327#[derive(SystemParam, Debug)]328pub struct DirectionalNavigation<'w> {329/// The currently focused entity.330pub focus: ResMut<'w, InputFocus>,331/// The directional navigation map containing manually defined connections between entities.332pub map: Res<'w, DirectionalNavigationMap>,333}334335impl<'w> DirectionalNavigation<'w> {336/// Navigates to the neighbor in a given direction from the current focus, if any.337///338/// Returns the new focus if successful.339/// Returns an error if there is no focus set or if there is no neighbor in the requested direction.340///341/// If the result was `Ok`, the [`InputFocus`] resource is updated to the new focus as part of this method call.342pub fn navigate(343&mut self,344direction: CompassOctant,345) -> Result<Entity, DirectionalNavigationError> {346if let Some(current_focus) = self.focus.0 {347// Respect manual edges first348if let Some(new_focus) = self.map.get_neighbor(current_focus, direction) {349self.focus.set(new_focus);350Ok(new_focus)351} else {352Err(DirectionalNavigationError::NoNeighborInDirection {353current_focus,354direction,355})356}357} else {358Err(DirectionalNavigationError::NoFocus)359}360}361}362363/// An error that can occur when navigating between focusable entities using [directional navigation](crate::directional_navigation).364#[derive(Debug, PartialEq, Clone, Error)]365pub enum DirectionalNavigationError {366/// No focusable entity is currently set.367#[error("No focusable entity is currently set.")]368NoFocus,369/// No neighbor in the requested direction.370#[error("No neighbor from {current_focus} in the {direction:?} direction.")]371NoNeighborInDirection {372/// The entity that was the focus when the error occurred.373current_focus: Entity,374/// The direction in which the navigation was attempted.375direction: CompassOctant,376},377}378379/// A focusable area with position and size information.380///381/// This struct represents a UI element used during directional navigation,382/// containing its entity ID, center position, and size for spatial navigation calculations.383///384/// The term "focusable area" avoids confusion with UI `Node` components in `bevy_ui`.385#[derive(Debug, Clone, Copy, PartialEq)]386#[cfg_attr(387feature = "bevy_reflect",388derive(Reflect),389reflect(Debug, PartialEq, Clone)390)]391pub struct FocusableArea {392/// The entity identifier for this focusable area.393pub entity: Entity,394/// The center position in global coordinates.395pub position: Vec2,396/// The size (width, height) of the area.397pub size: Vec2,398}399400/// Trait for extracting position and size from navigable UI components.401///402/// This allows the auto-navigation system to work with different UI implementations403/// as long as they can provide position and size information.404pub trait Navigable {405/// Returns the center position and size in global coordinates.406fn get_bounds(&self) -> (Vec2, Vec2);407}408409/// Automatically generates directional navigation edges for a collection of nodes.410///411/// This function takes a slice of navigation nodes with their positions and sizes, and populates412/// the navigation map with edges to the nearest neighbor in each compass direction.413/// Manual edges already in the map are preserved and not overwritten.414///415/// # Arguments416///417/// * `nav_map` - The navigation map to populate418/// * `nodes` - A slice of [`FocusableArea`] structs containing entity, position, and size data419/// * `config` - Configuration for the auto-generation algorithm420///421/// # Example422///423/// ```rust424/// # use bevy_input_focus::directional_navigation::*;425/// # use bevy_ecs::entity::Entity;426/// # use bevy_math::Vec2;427/// let mut nav_map = DirectionalNavigationMap::default();428/// let config = AutoNavigationConfig::default();429///430/// let nodes = vec![431/// FocusableArea { entity: Entity::PLACEHOLDER, position: Vec2::new(100.0, 100.0), size: Vec2::new(50.0, 50.0) },432/// FocusableArea { entity: Entity::PLACEHOLDER, position: Vec2::new(200.0, 100.0), size: Vec2::new(50.0, 50.0) },433/// ];434///435/// auto_generate_navigation_edges(&mut nav_map, &nodes, &config);436/// ```437pub fn auto_generate_navigation_edges(438nav_map: &mut DirectionalNavigationMap,439nodes: &[FocusableArea],440config: &AutoNavigationConfig,441) {442// For each node, find best neighbor in each direction443for origin in nodes {444for octant in [445CompassOctant::North,446CompassOctant::NorthEast,447CompassOctant::East,448CompassOctant::SouthEast,449CompassOctant::South,450CompassOctant::SouthWest,451CompassOctant::West,452CompassOctant::NorthWest,453] {454// Skip if manual edge already exists (check inline to avoid borrow issues)455if nav_map456.get_neighbors(origin.entity)457.and_then(|neighbors| neighbors.get(octant))458.is_some()459{460continue; // Respect manual override461}462463// Find best candidate in this direction464let best_candidate = find_best_candidate(origin, octant, nodes, config);465466// Add edge if we found a valid candidate467if let Some(neighbor) = best_candidate {468nav_map.add_edge(origin.entity, neighbor, octant);469}470}471}472}473474#[cfg(test)]475mod tests {476use alloc::vec;477use bevy_ecs::system::RunSystemOnce;478479use super::*;480481#[test]482fn setting_and_getting_nav_neighbors() {483let mut neighbors = NavNeighbors::EMPTY;484assert_eq!(neighbors.get(CompassOctant::SouthEast), None);485486neighbors.set(CompassOctant::SouthEast, Entity::PLACEHOLDER);487488for i in 0..8 {489if i == CompassOctant::SouthEast.to_index() {490assert_eq!(491neighbors.get(CompassOctant::SouthEast),492Some(Entity::PLACEHOLDER)493);494} else {495assert_eq!(neighbors.get(CompassOctant::from_index(i).unwrap()), None);496}497}498}499500#[test]501fn simple_set_and_get_navmap() {502let mut world = World::new();503let a = world.spawn_empty().id();504let b = world.spawn_empty().id();505506let mut map = DirectionalNavigationMap::default();507map.add_edge(a, b, CompassOctant::SouthEast);508509assert_eq!(map.get_neighbor(a, CompassOctant::SouthEast), Some(b));510assert_eq!(511map.get_neighbor(b, CompassOctant::SouthEast.opposite()),512None513);514}515516#[test]517fn symmetrical_edges() {518let mut world = World::new();519let a = world.spawn_empty().id();520let b = world.spawn_empty().id();521522let mut map = DirectionalNavigationMap::default();523map.add_symmetrical_edge(a, b, CompassOctant::North);524525assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b));526assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a));527}528529#[test]530fn remove_nodes() {531let mut world = World::new();532let a = world.spawn_empty().id();533let b = world.spawn_empty().id();534535let mut map = DirectionalNavigationMap::default();536map.add_edge(a, b, CompassOctant::North);537map.add_edge(b, a, CompassOctant::South);538539assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b));540assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a));541542map.remove(b);543544assert_eq!(map.get_neighbor(a, CompassOctant::North), None);545assert_eq!(map.get_neighbor(b, CompassOctant::South), None);546}547548#[test]549fn remove_multiple_nodes() {550let mut world = World::new();551let a = world.spawn_empty().id();552let b = world.spawn_empty().id();553let c = world.spawn_empty().id();554555let mut map = DirectionalNavigationMap::default();556map.add_edge(a, b, CompassOctant::North);557map.add_edge(b, a, CompassOctant::South);558map.add_edge(b, c, CompassOctant::East);559map.add_edge(c, b, CompassOctant::West);560561let mut to_remove = EntityHashSet::default();562to_remove.insert(b);563to_remove.insert(c);564565map.remove_multiple(to_remove);566567assert_eq!(map.get_neighbor(a, CompassOctant::North), None);568assert_eq!(map.get_neighbor(b, CompassOctant::South), None);569assert_eq!(map.get_neighbor(b, CompassOctant::East), None);570assert_eq!(map.get_neighbor(c, CompassOctant::West), None);571}572573#[test]574fn edges() {575let mut world = World::new();576let a = world.spawn_empty().id();577let b = world.spawn_empty().id();578let c = world.spawn_empty().id();579580let mut map = DirectionalNavigationMap::default();581map.add_edges(&[a, b, c], CompassOctant::East);582583assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b));584assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c));585assert_eq!(map.get_neighbor(c, CompassOctant::East), None);586587assert_eq!(map.get_neighbor(a, CompassOctant::West), None);588assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a));589assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b));590}591592#[test]593fn looping_edges() {594let mut world = World::new();595let a = world.spawn_empty().id();596let b = world.spawn_empty().id();597let c = world.spawn_empty().id();598599let mut map = DirectionalNavigationMap::default();600map.add_looping_edges(&[a, b, c], CompassOctant::East);601602assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b));603assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c));604assert_eq!(map.get_neighbor(c, CompassOctant::East), Some(a));605606assert_eq!(map.get_neighbor(a, CompassOctant::West), Some(c));607assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a));608assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b));609}610611#[test]612fn manual_nav_with_system_param() {613let mut world = World::new();614let a = world.spawn_empty().id();615let b = world.spawn_empty().id();616let c = world.spawn_empty().id();617618let mut map = DirectionalNavigationMap::default();619map.add_looping_edges(&[a, b, c], CompassOctant::East);620621world.insert_resource(map);622623let mut focus = InputFocus::default();624focus.set(a);625world.insert_resource(focus);626627let config = AutoNavigationConfig::default();628world.insert_resource(config);629630assert_eq!(world.resource::<InputFocus>().get(), Some(a));631632fn navigate_east(mut nav: DirectionalNavigation) {633nav.navigate(CompassOctant::East).unwrap();634}635636world.run_system_once(navigate_east).unwrap();637assert_eq!(world.resource::<InputFocus>().get(), Some(b));638639world.run_system_once(navigate_east).unwrap();640assert_eq!(world.resource::<InputFocus>().get(), Some(c));641642world.run_system_once(navigate_east).unwrap();643assert_eq!(world.resource::<InputFocus>().get(), Some(a));644}645646#[test]647fn test_auto_generate_navigation_edges() {648let mut nav_map = DirectionalNavigationMap::default();649let config = AutoNavigationConfig::default();650651// Create a 2x2 grid of nodes (using UI coordinates: smaller Y = higher on screen)652let node_a = Entity::from_bits(1); // Top-left653let node_b = Entity::from_bits(2); // Top-right654let node_c = Entity::from_bits(3); // Bottom-left655let node_d = Entity::from_bits(4); // Bottom-right656657let nodes = vec![658FocusableArea {659entity: node_a,660position: Vec2::new(0.0, 0.0),661size: Vec2::new(50.0, 50.0),662}, // Top-left663FocusableArea {664entity: node_b,665position: Vec2::new(100.0, 0.0),666size: Vec2::new(50.0, 50.0),667}, // Top-right668FocusableArea {669entity: node_c,670position: Vec2::new(0.0, 100.0),671size: Vec2::new(50.0, 50.0),672}, // Bottom-left673FocusableArea {674entity: node_d,675position: Vec2::new(100.0, 100.0),676size: Vec2::new(50.0, 50.0),677}, // Bottom-right678];679680auto_generate_navigation_edges(&mut nav_map, &nodes, &config);681682// Test horizontal navigation683assert_eq!(684nav_map.get_neighbor(node_a, CompassOctant::East),685Some(node_b)686);687assert_eq!(688nav_map.get_neighbor(node_b, CompassOctant::West),689Some(node_a)690);691692// Test vertical navigation693assert_eq!(694nav_map.get_neighbor(node_a, CompassOctant::South),695Some(node_c)696);697assert_eq!(698nav_map.get_neighbor(node_c, CompassOctant::North),699Some(node_a)700);701702// Test diagonal navigation703assert_eq!(704nav_map.get_neighbor(node_a, CompassOctant::SouthEast),705Some(node_d)706);707}708709#[test]710fn test_auto_generate_respects_manual_edges() {711let mut nav_map = DirectionalNavigationMap::default();712let config = AutoNavigationConfig::default();713714let node_a = Entity::from_bits(1);715let node_b = Entity::from_bits(2);716let node_c = Entity::from_bits(3);717718// Manually set an edge from A to C (skipping B)719nav_map.add_edge(node_a, node_c, CompassOctant::East);720721let nodes = vec![722FocusableArea {723entity: node_a,724position: Vec2::new(0.0, 0.0),725size: Vec2::new(50.0, 50.0),726},727FocusableArea {728entity: node_b,729position: Vec2::new(50.0, 0.0),730size: Vec2::new(50.0, 50.0),731}, // Closer732FocusableArea {733entity: node_c,734position: Vec2::new(100.0, 0.0),735size: Vec2::new(50.0, 50.0),736},737];738739auto_generate_navigation_edges(&mut nav_map, &nodes, &config);740741// The manual edge should be preserved, even though B is closer742assert_eq!(743nav_map.get_neighbor(node_a, CompassOctant::East),744Some(node_c)745);746}747748#[test]749fn test_edge_distance_vs_center_distance() {750let mut nav_map = DirectionalNavigationMap::default();751let config = AutoNavigationConfig::default();752753let left = Entity::from_bits(1);754let wide_top = Entity::from_bits(2);755let bottom = Entity::from_bits(3);756757let left_node = FocusableArea {758entity: left,759position: Vec2::new(100.0, 200.0),760size: Vec2::new(100.0, 100.0),761};762763let wide_top_node = FocusableArea {764entity: wide_top,765position: Vec2::new(350.0, 150.0),766size: Vec2::new(300.0, 80.0),767};768769let bottom_node = FocusableArea {770entity: bottom,771position: Vec2::new(270.0, 300.0),772size: Vec2::new(100.0, 80.0),773};774775let nodes = vec![left_node, wide_top_node, bottom_node];776777auto_generate_navigation_edges(&mut nav_map, &nodes, &config);778779assert_eq!(780nav_map.get_neighbor(left, CompassOctant::East),781Some(wide_top),782"Should navigate to wide_top not bottom, even though bottom's center is closer."783);784}785}786787788