From 7637cfff73b9c8a32aeb8b8b1eac371eff053ac2 Mon Sep 17 00:00:00 2001 From: Lucas Farias Date: Thu, 22 May 2025 21:49:28 -0300 Subject: [PATCH 01/14] Image Node Rotation --- Cargo.toml | 11 + crates/bevy_ui/src/render/debug_overlay.rs | 2 +- crates/bevy_ui/src/render/gradient.rs | 2 +- crates/bevy_ui/src/render/mod.rs | 31 ++- .../src/render/ui_texture_slice_pipeline.rs | 16 +- crates/bevy_ui/src/widget/image.rs | 14 +- examples/testbed/full_ui.rs | 17 +- examples/ui/image_node_rotation.rs | 235 ++++++++++++++++++ examples/ui/ui_texture_slice_flip_and_tile.rs | 16 +- 9 files changed, 302 insertions(+), 42 deletions(-) create mode 100644 examples/ui/image_node_rotation.rs diff --git a/Cargo.toml b/Cargo.toml index 3cd87c291c26a..bc674525fc409 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3418,6 +3418,17 @@ description = "An example for CSS Grid layout" category = "UI (User Interface)" wasm = true +[[example]] +name = "image_node_rotation" +path = "examples/ui/image_node_rotation.rs" +doc-scrape-examples = true + +[package.metadata.example.image_node_rotation] +name = "Image Node Rotation" +description = "Show the use of `ImageNode::rotation`" +category = "UI (User Interface)" +wasm = true + [[example]] name = "gradients" path = "examples/ui/gradients.rs" diff --git a/crates/bevy_ui/src/render/debug_overlay.rs b/crates/bevy_ui/src/render/debug_overlay.rs index 79001f3ba1982..c1b374d68c24d 100644 --- a/crates/bevy_ui/src/render/debug_overlay.rs +++ b/crates/bevy_ui/src/render/debug_overlay.rs @@ -102,7 +102,7 @@ pub fn extract_debug_overlay( item: ExtractedUiItem::Node { atlas_scaling: None, transform: transform.compute_matrix(), - flip_x: false, + rotation: 0., flip_y: false, border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), border_radius: uinode.border_radius(), diff --git a/crates/bevy_ui/src/render/gradient.rs b/crates/bevy_ui/src/render/gradient.rs index b908e148e9554..bd7f8effc1b9b 100644 --- a/crates/bevy_ui/src/render/gradient.rs +++ b/crates/bevy_ui/src/render/gradient.rs @@ -411,7 +411,7 @@ pub fn extract_gradients( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - flip_x: false, + rotation: 0., flip_y: false, border_radius: uinode.border_radius, border: uinode.border, diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 8b0d6bad879fe..94be13b5c93a3 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -235,7 +235,7 @@ pub enum NodeType { pub enum ExtractedUiItem { Node { atlas_scaling: Option, - flip_x: bool, + rotation: f32, flip_y: bool, /// Border radius of the UI node. /// Ordering: top left, top right, bottom right, bottom left. @@ -385,7 +385,7 @@ pub fn extract_uinode_background_colors( item: ExtractedUiItem::Node { atlas_scaling: None, transform: transform.compute_matrix(), - flip_x: false, + rotation: 0., flip_y: false, border: uinode.border(), border_radius: uinode.border_radius(), @@ -469,7 +469,7 @@ pub fn extract_uinode_images( item: ExtractedUiItem::Node { atlas_scaling, transform: transform.compute_matrix(), - flip_x: image.flip_x, + rotation: image.rotation, flip_y: image.flip_y, border: uinode.border, border_radius: uinode.border_radius, @@ -537,7 +537,7 @@ pub fn extract_uinode_borders( item: ExtractedUiItem::Node { atlas_scaling: None, transform: global_transform.compute_matrix(), - flip_x: false, + rotation: 0., flip_y: false, border: computed_node.border(), border_radius: computed_node.border_radius(), @@ -570,7 +570,7 @@ pub fn extract_uinode_borders( item: ExtractedUiItem::Node { transform: global_transform.compute_matrix(), atlas_scaling: None, - flip_x: false, + rotation: 0., flip_y: false, border: BorderRect::all(computed_node.outline_width()), border_radius: computed_node.outline_radius(), @@ -760,7 +760,7 @@ pub fn extract_viewport_nodes( item: ExtractedUiItem::Node { atlas_scaling: None, transform: transform.compute_matrix(), - flip_x: false, + rotation: 0., flip_y: false, border: uinode.border(), border_radius: uinode.border_radius(), @@ -1010,7 +1010,7 @@ pub fn extract_text_background_colors( item: ExtractedUiItem::Node { atlas_scaling: None, transform: transform * Mat4::from_translation(rect.center().extend(0.)), - flip_x: false, + rotation: 0., flip_y: false, border: uinode.border(), border_radius: uinode.border_radius(), @@ -1269,7 +1269,7 @@ pub fn prepare_uinodes( match &extracted_uinode.item { ExtractedUiItem::Node { atlas_scaling, - flip_x, + rotation, flip_y, border_radius, border, @@ -1287,8 +1287,12 @@ pub fn prepare_uinodes( let rect_size = uinode_rect.size().extend(1.0); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (*transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + (*transform + * Mat4::from_rotation_z(*rotation) + * (pos * rect_size).extend(1.)) + .xyz() + }); let points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); // Calculate the effect of clipping @@ -1358,13 +1362,6 @@ pub fn prepare_uinodes( let atlas_extent = atlas_scaling .map(|scaling| image.size_2d().as_vec2() * scaling) .unwrap_or(uinode_rect.max); - if *flip_x { - core::mem::swap(&mut uinode_rect.max.x, &mut uinode_rect.min.x); - positions_diff[0].x *= -1.; - positions_diff[1].x *= -1.; - positions_diff[2].x *= -1.; - positions_diff[3].x *= -1.; - } if *flip_y { core::mem::swap(&mut uinode_rect.max.y, &mut uinode_rect.min.y); positions_diff[0].y *= -1.; diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index 7d0fdb6a42f0c..6f279af82a66b 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -232,7 +232,7 @@ pub struct ExtractedUiTextureSlice { pub extracted_camera_entity: Entity, pub color: LinearRgba, pub image_scale_mode: SpriteImageMode, - pub flip_x: bool, + pub rotation: f32, pub flip_y: bool, pub inverse_scale_factor: f32, pub main_entity: MainEntity, @@ -323,7 +323,7 @@ pub fn extract_ui_texture_slices( extracted_camera_entity, image_scale_mode, atlas_rect, - flip_x: image.flip_x, + rotation: image.rotation, flip_y: image.flip_y, inverse_scale_factor: uinode.inverse_scale_factor, main_entity: entity.into(), @@ -506,8 +506,12 @@ pub fn prepare_ui_slices( let rect_size = uinode_rect.size().extend(1.0); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (texture_slices.transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + (texture_slices.transform + * Mat4::from_rotation_z(texture_slices.rotation) + * (pos * rect_size).extend(1.)) + .xyz() + }); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -605,10 +609,6 @@ pub fn prepare_ui_slices( (batch_image_size, [0., 0., 1., 1.]) }; - if texture_slices.flip_x { - atlas.swap(0, 2); - } - if texture_slices.flip_y { atlas.swap(1, 3); } diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index c65c4df354881..679ed42645950 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -25,8 +25,8 @@ pub struct ImageNode { pub image: Handle, /// The (optional) texture atlas used to render the image. pub texture_atlas: Option, - /// Whether the image should be flipped along its x-axis. - pub flip_x: bool, + /// Rotation of the image + pub rotation: f32, /// Whether the image should be flipped along its y-axis. pub flip_y: bool, /// An optional rectangle representing the region of the image to render, instead of rendering @@ -55,7 +55,7 @@ impl Default for ImageNode { texture_atlas: None, // This texture needs to be transparent by default, to avoid covering the background color image: TRANSPARENT_IMAGE_HANDLE, - flip_x: false, + rotation: 0., flip_y: false, rect: None, image_mode: NodeImageMode::Auto, @@ -80,7 +80,7 @@ impl ImageNode { Self { image: Handle::default(), color, - flip_x: false, + rotation: 0., flip_y: false, texture_atlas: None, rect: None, @@ -104,10 +104,10 @@ impl ImageNode { self } - /// Flip the image along its x-axis + /// Rotates the image #[must_use] - pub const fn with_flip_x(mut self) -> Self { - self.flip_x = true; + pub const fn with_rotation(mut self, rotation: f32) -> Self { + self.rotation = rotation; self } diff --git a/examples/testbed/full_ui.rs b/examples/testbed/full_ui.rs index 551785ca0fbad..a1f9fcc688ca3 100644 --- a/examples/testbed/full_ui.rs +++ b/examples/testbed/full_ui.rs @@ -1,6 +1,6 @@ //! This example illustrates the various features of Bevy UI. -use std::f32::consts::PI; +use std::f32::consts::{FRAC_PI_2, PI}; use accesskit::{Node as Accessible, Role}; use bevy::{ @@ -375,13 +375,20 @@ fn setup(mut commands: Commands, asset_server: Res) { }) .insert(Pickable::IGNORE) .with_children(|parent| { - for (flip_x, flip_y) in - [(false, false), (false, true), (true, true), (true, false)] - { + for (rotation, flip_y) in [ + (0., false), + (FRAC_PI_2, false), + (PI, false), + (FRAC_PI_2 + PI, false), + (0., true), + (FRAC_PI_2, true), + (PI, true), + (FRAC_PI_2 + PI, true), + ] { parent.spawn(( ImageNode { image: asset_server.load("branding/icon.png"), - flip_x, + rotation, flip_y, ..default() }, diff --git a/examples/ui/image_node_rotation.rs b/examples/ui/image_node_rotation.rs new file mode 100644 index 0000000000000..316593647f691 --- /dev/null +++ b/examples/ui/image_node_rotation.rs @@ -0,0 +1,235 @@ +//! Shows usage of `ImageNode::rotation` + +#[cfg(feature = "bevy_ui_debug")] +use bevy::prelude::UiDebugOptions; +use bevy::{ + app::{App, Startup, Update}, + color::Color, + core_pipeline::core_2d::Camera2d, + ecs::{ + children, + component::Component, + entity::Entity, + query::With, + system::{Commands, Query, Res, ResMut, Single}, + }, + input::{keyboard::KeyCode, ButtonInput}, + math::ops, + prelude::SpawnRelated, + state::{ + app::AppExtStates, + state::{NextState, OnEnter, OnExit, State, States}, + }, + time::Time, + ui::{widget::ImageNode, FlexDirection, JustifyContent, Node, PositionType, Val}, + DefaultPlugins, +}; +use bevy_asset::AssetServer; + +const IMAGE_NODE_SIZE: f32 = 128.; + +fn main() { + let mut app = App::new(); + + #[cfg(feature = "bevy_ui_debug")] + app.insert_resource(UiDebugOptions { + enabled: true, + show_clipped: true, + show_hidden: true, + line_width: 3., + }); + + app.add_plugins(DefaultPlugins) + .init_state::() + .add_systems(Startup, setup) + .add_systems(Update, (switch_modes, rotate_image_nodes)) + .add_systems(OnEnter(Mode::Image), image_mode) + .add_systems(OnExit(Mode::Image), drop_previos_ui) + .add_systems(OnEnter(Mode::Slices), image_mode) + .add_systems(OnExit(Mode::Slices), drop_previos_ui) + .run(); +} + +#[derive(Component)] +struct UiMarker; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, States)] +enum Mode { + #[default] + Image, + Slices, +} + +impl Mode { + fn next(&self) -> Self { + match self { + Self::Image => Self::Slices, + Self::Slices => Self::Image, + } + } +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d); +} + +fn switch_modes( + keys: Res>, + current_state: Res>, + mut next_state: ResMut>, +) { + if keys.just_pressed(KeyCode::KeyQ) { + next_state.set(dbg!(current_state.next())); + } +} + +fn rotate_image_nodes(mut image_nodes: Query<&mut ImageNode>, time: Res