Skip to content

Commit f572ad9

Browse files
snk-git-hubcart
andauthored
Directional navigation now considers UiTransform rotation (#22399)
# Objective Fixes #22234 The directional navigation system was ignoring `UiTransform` rotation and scaling when calculating which node to navigate to. This caused navigation to select incorrect nodes when they were rotated or scaled, because the system used unrotated layout positions instead of the visual positions seen by users. ## Solution - Added `get_rotated_bounds()` helper function to calculate the axis-aligned bounding box of a rotated rectangle - Updated `get_navigable_nodes()` to apply both rotation and scale transforms from `UiGlobalTransform` when creating `FocusableArea` structs - Updated `entity_to_camera_and_focusable_area()` to apply rotation and scale transforms - Used `bevy_math::ops` instead of `f32` methods for deterministic trigonometry The fix ensures that directional navigation uses the visual position of nodes (after transforms) rather than just the layout position, so users navigate to the button they actually see on screen. ## Testing - Tested with `auto_directional_navigation` example - navigation works correctly with scattered button layouts ![Screencast From 2026-01-06 11-37-04](https://github.com/user-attachments/assets/fcaf5d24-f337-4f2d-b1e9-c08651bd9d97) --------- Co-authored-by: Carter Anderson <[email protected]>
1 parent cd977df commit f572ad9

2 files changed

Lines changed: 32 additions & 7 deletions

File tree

crates/bevy_ui/src/auto_directional_navigation.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use crate::{ComputedNode, ComputedUiTargetCamera, UiGlobalTransform};
77
use bevy_camera::visibility::InheritedVisibility;
88
use bevy_ecs::{prelude::*, system::SystemParam};
9-
use bevy_math::CompassOctant;
9+
use bevy_math::{ops, CompassOctant, Vec2};
1010

1111
use bevy_input_focus::{
1212
directional_navigation::{
@@ -184,12 +184,13 @@ impl<'w, 's> AutoDirectionalNavigator<'w, 's> {
184184
if let Some(tc) = computed_target_camera.get()
185185
&& tc == target_camera
186186
{
187-
let (_scale, _rotation, translation) =
188-
transform.to_scale_angle_translation();
187+
let (scale, rotation, translation) = transform.to_scale_angle_translation();
188+
let scaled_size = computed.size() * computed.inverse_scale_factor() * scale;
189+
let rotated_size = get_rotated_bounds(scaled_size, rotation);
189190
Some(FocusableArea {
190191
entity,
191192
position: translation * computed.inverse_scale_factor(),
192-
size: computed.size() * computed.inverse_scale_factor(),
193+
size: rotated_size,
193194
})
194195
} else {
195196
// The node either does not have a target camera or it is not the same as the desired one.
@@ -212,13 +213,15 @@ impl<'w, 's> AutoDirectionalNavigator<'w, 's> {
212213
None,
213214
|(entity, computed_target_camera, computed, transform)| {
214215
if let Some(target_camera) = computed_target_camera.get() {
215-
let (_scale, _rotation, translation) = transform.to_scale_angle_translation();
216+
let (scale, rotation, translation) = transform.to_scale_angle_translation();
217+
let scaled_size = computed.size() * computed.inverse_scale_factor() * scale;
218+
let rotated_size = get_rotated_bounds(scaled_size, rotation);
216219
Some((
217220
target_camera,
218221
FocusableArea {
219222
entity,
220223
position: translation * computed.inverse_scale_factor(),
221-
size: computed.size() * computed.inverse_scale_factor(),
224+
size: rotated_size,
222225
},
223226
))
224227
} else {
@@ -228,3 +231,15 @@ impl<'w, 's> AutoDirectionalNavigator<'w, 's> {
228231
)
229232
}
230233
}
234+
235+
fn get_rotated_bounds(size: Vec2, rotation: f32) -> Vec2 {
236+
if rotation == 0.0 {
237+
return size;
238+
}
239+
let cos_r = ops::cos(rotation).abs();
240+
let sin_r = ops::sin(rotation).abs();
241+
Vec2::new(
242+
size.x * cos_r + size.y * sin_r,
243+
size.x * sin_r + size.y * cos_r,
244+
)
245+
}

examples/ui/auto_directional_navigation.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use bevy::{
2020
directional_navigation::{AutoNavigationConfig, DirectionalNavigationPlugin},
2121
InputDispatchPlugin, InputFocus, InputFocusVisible,
2222
},
23-
math::{CompassOctant, Dir2},
23+
math::{CompassOctant, Dir2, Rot2},
2424
picking::{
2525
backend::HitData,
2626
pointer::{Location, PointerId},
@@ -210,6 +210,15 @@ fn setup_scattered_ui(mut commands: Commands, mut input_focus: ResMut<InputFocus
210210

211211
let mut first_button = None;
212212
for (i, (x, y)) in button_positions.iter().enumerate() {
213+
let transform = if i == 4 {
214+
UiTransform {
215+
scale: Vec2::splat(1.2),
216+
rotation: Rot2::FRAC_PI_2,
217+
..default()
218+
}
219+
} else {
220+
UiTransform::IDENTITY
221+
};
213222
let button_entity = commands
214223
.spawn((
215224
Button,
@@ -225,6 +234,7 @@ fn setup_scattered_ui(mut commands: Commands, mut input_focus: ResMut<InputFocus
225234
border_radius: BorderRadius::all(px(12)),
226235
..default()
227236
},
237+
transform,
228238
// This is the key: just add this component for automatic navigation!
229239
AutoDirectionalNavigation::default(),
230240
ResetTimer::default(),

0 commit comments

Comments
 (0)