diff --git a/Cargo.toml b/Cargo.toml index 000bebe134..703877983c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3543,6 +3543,17 @@ description = "Illustrates how to use 9 Slicing for TextureAtlases in UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_transform" +path = "examples/ui/ui_transform.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_transform] +name = "UI Transform" +description = "An example demonstrating how to translate, rotate and scale UI elements." +category = "UI (User Interface)" +wasm = true + [[example]] name = "viewport_debug" path = "examples/ui/viewport_debug.rs" diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index 637ed943f2..78e80717a2 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -1,6 +1,7 @@ use crate::{ experimental::UiChildren, prelude::{Button, Label}, + ui_transform::UiGlobalTransform, widget::{ImageNode, TextUiReader}, ComputedNode, }; @@ -13,11 +14,9 @@ use bevy_ecs::{ system::{Commands, Query}, world::Ref, }; -use bevy_math::Vec3Swizzles; -use bevy_render::camera::CameraUpdateSystems; -use bevy_transform::prelude::GlobalTransform; use accesskit::{Node, Rect, Role}; +use bevy_render::camera::CameraUpdateSystems; fn calc_label( text_reader: &mut TextUiReader, @@ -40,12 +39,12 @@ fn calc_bounds( mut nodes: Query<( &mut AccessibilityNode, Ref, - Ref, + Ref, )>, ) { for (mut accessible, node, transform) in &mut nodes { if node.is_changed() || transform.is_changed() { - let center = transform.translation().xy(); + let center = transform.translation; let half_size = 0.5 * node.size; let min = center - half_size; let max = center + half_size; diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 9242bf1380..f55cbb92b8 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,18 +1,21 @@ -use crate::{CalculatedClip, ComputedNode, ComputedNodeTarget, ResolvedBorderRadius, UiStack}; +use crate::{ + picking_backend::clip_check_recursive, ui_transform::UiGlobalTransform, ComputedNode, + ComputedNodeTarget, Node, UiStack, +}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::{ContainsEntity, Entity}, + hierarchy::ChildOf, prelude::{Component, With}, query::QueryData, reflect::ReflectComponent, system::{Local, Query, Res}, }; use bevy_input::{mouse::MouseButton, touch::Touches, ButtonInput}; -use bevy_math::{Rect, Vec2}; +use bevy_math::Vec2; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::InheritedVisibility}; -use bevy_transform::components::GlobalTransform; use bevy_window::{PrimaryWindow, Window}; use smallvec::SmallVec; @@ -67,12 +70,12 @@ impl Default for Interaction { } } -/// A component storing the position of the mouse relative to the node, (0., 0.) being the top-left corner and (1., 1.) being the bottom-right -/// If the mouse is not over the node, the value will go beyond the range of (0., 0.) to (1., 1.) +/// A component storing the position of the mouse relative to the node, (0., 0.) being the center and (0.5, 0.5) being the bottom-right +/// If the mouse is not over the node, the value will go beyond the range of (-0.5, -0.5) to (0.5, 0.5) /// /// It can be used alongside [`Interaction`] to get the position of the press. /// -/// The component is updated when it is in the same entity with [`Node`](crate::Node). +/// The component is updated when it is in the same entity with [`Node`]. #[derive(Component, Copy, Clone, Default, PartialEq, Debug, Reflect)] #[reflect(Component, Default, PartialEq, Debug, Clone)] #[cfg_attr( @@ -81,8 +84,8 @@ impl Default for Interaction { reflect(Serialize, Deserialize) )] pub struct RelativeCursorPosition { - /// Visible area of the Node relative to the size of the entire Node. - pub normalized_visible_node_rect: Rect, + /// True if the cursor position is over an unclipped area of the Node. + pub cursor_over: bool, /// Cursor position relative to the size and position of the Node. /// A None value indicates that the cursor position is unknown. pub normalized: Option, @@ -90,9 +93,8 @@ pub struct RelativeCursorPosition { impl RelativeCursorPosition { /// A helper function to check if the mouse is over the node - pub fn mouse_over(&self) -> bool { - self.normalized - .is_some_and(|position| self.normalized_visible_node_rect.contains(position)) + pub fn cursor_over(&self) -> bool { + self.cursor_over } } @@ -133,11 +135,10 @@ pub struct State { pub struct NodeQuery { entity: Entity, node: &'static ComputedNode, - global_transform: &'static GlobalTransform, + transform: &'static UiGlobalTransform, interaction: Option<&'static mut Interaction>, relative_cursor_position: Option<&'static mut RelativeCursorPosition>, focus_policy: Option<&'static FocusPolicy>, - calculated_clip: Option<&'static CalculatedClip>, inherited_visibility: Option<&'static InheritedVisibility>, target_camera: &'static ComputedNodeTarget, } @@ -154,6 +155,8 @@ pub fn ui_focus_system( touches_input: Res, ui_stack: Res, mut node_query: Query, + clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>, + child_of_query: Query<&ChildOf>, ) { let primary_window = primary_window.iter().next(); @@ -234,46 +237,30 @@ pub fn ui_focus_system( } let camera_entity = node.target_camera.camera()?; - let node_rect = Rect::from_center_size( - node.global_transform.translation().truncate(), - node.node.size(), - ); - - // Intersect with the calculated clip rect to find the bounds of the visible region of the node - let visible_rect = node - .calculated_clip - .map(|clip| node_rect.intersect(clip.clip)) - .unwrap_or(node_rect); - let cursor_position = camera_cursor_positions.get(&camera_entity); + let contains_cursor = cursor_position.is_some_and(|point| { + node.node.contains_point(*node.transform, *point) + && clip_check_recursive(*point, *entity, &clipping_query, &child_of_query) + }); + // The mouse position relative to the node - // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner + // (-0.5, -0.5) is the top-left corner, (0.5, 0.5) is the bottom-right corner // Coordinates are relative to the entire node, not just the visible region. - let relative_cursor_position = cursor_position.and_then(|cursor_position| { + let normalized_cursor_position = cursor_position.and_then(|cursor_position| { // ensure node size is non-zero in all dimensions, otherwise relative position will be // +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to // false positives for mouse_over (#12395) - (node_rect.size().cmpgt(Vec2::ZERO).all()) - .then_some((*cursor_position - node_rect.min) / node_rect.size()) + node.node.normalize_point(*node.transform, *cursor_position) }); // If the current cursor position is within the bounds of the node's visible area, consider it for // clicking let relative_cursor_position_component = RelativeCursorPosition { - normalized_visible_node_rect: visible_rect.normalize(node_rect), - normalized: relative_cursor_position, + cursor_over: contains_cursor, + normalized: normalized_cursor_position, }; - let contains_cursor = relative_cursor_position_component.mouse_over() - && cursor_position.is_some_and(|point| { - pick_rounded_rect( - *point - node_rect.center(), - node_rect.size(), - node.node.border_radius, - ) - }); - // Save the relative cursor position to the correct component if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position { @@ -284,7 +271,8 @@ pub fn ui_focus_system( Some(*entity) } else { if let Some(mut interaction) = node.interaction { - if *interaction == Interaction::Hovered || (relative_cursor_position.is_none()) + if *interaction == Interaction::Hovered + || (normalized_cursor_position.is_none()) { interaction.set_if_neq(Interaction::None); } @@ -334,26 +322,3 @@ pub fn ui_focus_system( } } } - -// Returns true if `point` (relative to the rectangle's center) is within the bounds of a rounded rectangle with -// the given size and border radius. -// -// Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles. -pub(crate) fn pick_rounded_rect( - point: Vec2, - size: Vec2, - border_radius: ResolvedBorderRadius, -) -> bool { - let [top, bottom] = if point.x < 0. { - [border_radius.top_left, border_radius.bottom_left] - } else { - [border_radius.top_right, border_radius.bottom_right] - }; - let r = if point.y < 0. { top } else { bottom }; - - let corner_to_point = point.abs() - 0.5 * size; - let q = corner_to_point + r; - let l = q.max(Vec2::ZERO).length(); - let m = q.max_element().min(0.); - l + m - r < 0. -} diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 079e73bb49..53c03113b9 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -448,6 +448,8 @@ impl RepeatedGridTrack { #[cfg(test)] mod tests { + use bevy_math::Vec2; + use super::*; #[test] @@ -523,7 +525,7 @@ mod tests { grid_column: GridPlacement::start(4), grid_row: GridPlacement::span(3), }; - let viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.)); + let viewport_values = LayoutContext::new(1.0, Vec2::new(800., 600.)); let taffy_style = from_node(&node, &viewport_values, false); assert_eq!(taffy_style.display, taffy::style::Display::Flex); assert_eq!(taffy_style.box_sizing, taffy::style::BoxSizing::ContentBox); @@ -661,7 +663,7 @@ mod tests { #[test] fn test_into_length_percentage() { use taffy::style::LengthPercentage; - let context = LayoutContext::new(2.0, bevy_math::Vec2::new(800., 600.)); + let context = LayoutContext::new(2.0, Vec2::new(800., 600.)); let cases = [ (Val::Auto, LengthPercentage::Length(0.)), (Val::Percent(1.), LengthPercentage::Percent(0.01)), diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index b38241a95a..a5e53d981e 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1,5 +1,6 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, + ui_transform::{UiGlobalTransform, UiTransform}, BorderRadius, ComputedNode, ComputedNodeTarget, ContentSize, Display, LayoutConfig, Node, Outline, OverflowAxis, ScrollPosition, }; @@ -12,9 +13,9 @@ use bevy_ecs::{ system::{Commands, Query, ResMut}, world::Ref, }; -use bevy_math::Vec2; + +use bevy_math::{Affine2, Vec2}; use bevy_sprite::BorderRect; -use bevy_transform::components::Transform; use thiserror::Error; use tracing::warn; use ui_surface::UiSurface; @@ -81,9 +82,10 @@ pub fn ui_layout_system( )>, computed_node_query: Query<(Entity, Option>), With>, ui_children: UiChildren, - mut node_transform_query: Query<( + mut node_update_query: Query<( &mut ComputedNode, - &mut Transform, + &UiTransform, + &mut UiGlobalTransform, &Node, Option<&LayoutConfig>, Option<&BorderRadius>, @@ -175,7 +177,8 @@ with UI components as a child of an entity without UI components, your UI layout &mut ui_surface, true, computed_target.physical_size().as_vec2(), - &mut node_transform_query, + Affine2::IDENTITY, + &mut node_update_query, &ui_children, computed_target.scale_factor.recip(), Vec2::ZERO, @@ -190,9 +193,11 @@ with UI components as a child of an entity without UI components, your UI layout ui_surface: &mut UiSurface, inherited_use_rounding: bool, target_size: Vec2, - node_transform_query: &mut Query<( + mut inherited_transform: Affine2, + node_update_query: &mut Query<( &mut ComputedNode, - &mut Transform, + &UiTransform, + &mut UiGlobalTransform, &Node, Option<&LayoutConfig>, Option<&BorderRadius>, @@ -206,13 +211,14 @@ with UI components as a child of an entity without UI components, your UI layout ) { if let Ok(( mut node, - mut transform, + transform, + mut global_transform, style, maybe_layout_config, maybe_border_radius, maybe_outline, maybe_scroll_position, - )) = node_transform_query.get_mut(entity) + )) = node_update_query.get_mut(entity) { let use_rounding = maybe_layout_config .map(|layout_config| layout_config.use_rounding) @@ -224,10 +230,11 @@ with UI components as a child of an entity without UI components, your UI layout let layout_size = Vec2::new(layout.size.width, layout.size.height); + // Taffy layout position of the top-left corner of the node, relative to its parent. let layout_location = Vec2::new(layout.location.x, layout.location.y); - // The position of the center of the node, stored in the node's transform - let node_center = + // The position of the center of the node relative to its top-left corner. + let local_center = layout_location - parent_scroll_position + 0.5 * (layout_size - parent_size); // only trigger change detection when the new values are different @@ -253,6 +260,16 @@ with UI components as a child of an entity without UI components, your UI layout node.bypass_change_detection().border = taffy_rect_to_border_rect(layout.border); node.bypass_change_detection().padding = taffy_rect_to_border_rect(layout.padding); + // Computer the node's new global transform + let mut local_transform = + transform.compute_affine(inverse_target_scale_factor, layout_size, target_size); + local_transform.translation += local_center; + inherited_transform *= local_transform; + + if inherited_transform != **global_transform { + *global_transform = inherited_transform.into(); + } + if let Some(border_radius) = maybe_border_radius { // We don't trigger change detection for changes to border radius node.bypass_change_detection().border_radius = border_radius.resolve( @@ -290,10 +307,6 @@ with UI components as a child of an entity without UI components, your UI layout .max(0.); } - if transform.translation.truncate() != node_center { - transform.translation = node_center.extend(0.); - } - let scroll_position: Vec2 = maybe_scroll_position .map(|scroll_pos| { Vec2::new( @@ -333,7 +346,8 @@ with UI components as a child of an entity without UI components, your UI layout ui_surface, use_rounding, target_size, - node_transform_query, + inherited_transform, + node_update_query, ui_children, inverse_target_scale_factor, layout_size, @@ -356,10 +370,7 @@ mod tests { use bevy_platform::collections::HashMap; use bevy_render::{camera::ManualTextureViews, prelude::Camera}; use bevy_transform::systems::mark_dirty_trees; - use bevy_transform::{ - prelude::GlobalTransform, - systems::{propagate_parent_transforms, sync_simple_transforms}, - }; + use bevy_transform::systems::{propagate_parent_transforms, sync_simple_transforms}; use bevy_utils::prelude::default; use bevy_window::{ PrimaryWindow, Window, WindowCreated, WindowResized, WindowResolution, @@ -684,23 +695,20 @@ mod tests { ui_schedule.run(&mut world); let overlap_check = world - .query_filtered::<(Entity, &ComputedNode, &GlobalTransform), Without>() + .query_filtered::<(Entity, &ComputedNode, &UiGlobalTransform), Without>() .iter(&world) .fold( Option::<(Rect, bool)>::None, - |option_rect, (entity, node, global_transform)| { - let current_rect = Rect::from_center_size( - global_transform.translation().truncate(), - node.size(), - ); + |option_rect, (entity, node, transform)| { + let current_rect = Rect::from_center_size(transform.translation, node.size()); assert!( current_rect.height().abs() + current_rect.width().abs() > 0., "root ui node {entity} doesn't have a logical size" ); assert_ne!( - global_transform.affine(), - GlobalTransform::default().affine(), - "root ui node {entity} global transform is not populated" + *transform, + UiGlobalTransform::default(), + "root ui node {entity} transform is not populated" ); let Some((rect, is_overlapping)) = option_rect else { return Some((current_rect, false)); diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 4ce6359206..be0649c038 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -18,6 +18,7 @@ pub mod widget; pub mod gradients; #[cfg(feature = "bevy_ui_picking_backend")] pub mod picking_backend; +pub mod ui_transform; use bevy_derive::{Deref, DerefMut}; #[cfg(feature = "bevy_ui_picking_backend")] @@ -42,6 +43,7 @@ pub use measurement::*; pub use render::*; pub use ui_material::*; pub use ui_node::*; +pub use ui_transform::*; use widget::{ImageNode, ImageNodeSize, ViewportNode}; @@ -64,6 +66,7 @@ pub mod prelude { gradients::*, ui_material::*, ui_node::*, + ui_transform::*, widget::{Button, ImageNode, Label, NodeImageMode, ViewportNode}, Interaction, MaterialNode, UiMaterialPlugin, UiScale, }, diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 26b84c6005..5647baee12 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -24,14 +24,13 @@ #![deny(missing_docs)] -use crate::{focus::pick_rounded_rect, prelude::*, UiStack}; +use crate::{prelude::*, ui_transform::UiGlobalTransform, UiStack}; use bevy_app::prelude::*; use bevy_ecs::{prelude::*, query::QueryData}; -use bevy_math::{Rect, Vec2}; +use bevy_math::Vec2; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::prelude::*; -use bevy_transform::prelude::*; use bevy_window::PrimaryWindow; use bevy_picking::backend::prelude::*; @@ -91,9 +90,8 @@ impl Plugin for UiPickingPlugin { pub struct NodeQuery { entity: Entity, node: &'static ComputedNode, - global_transform: &'static GlobalTransform, + transform: &'static UiGlobalTransform, pickable: Option<&'static Pickable>, - calculated_clip: Option<&'static CalculatedClip>, inherited_visibility: Option<&'static InheritedVisibility>, target_camera: &'static ComputedNodeTarget, } @@ -110,6 +108,8 @@ pub fn ui_picking( ui_stack: Res, node_query: Query, mut output: EventWriter, + clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>, + child_of_query: Query<&ChildOf>, ) { // For each camera, the pointer and its position let mut pointer_pos_by_camera = HashMap::>::default(); @@ -181,43 +181,33 @@ pub fn ui_picking( continue; }; - let node_rect = Rect::from_center_size( - node.global_transform.translation().truncate(), - node.node.size(), - ); - // Nodes with Display::None have a (0., 0.) logical rect and can be ignored - if node_rect.size() == Vec2::ZERO { + if node.node.size() == Vec2::ZERO { continue; } - // Intersect with the calculated clip rect to find the bounds of the visible region of the node - let visible_rect = node - .calculated_clip - .map(|clip| node_rect.intersect(clip.clip)) - .unwrap_or(node_rect); - let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity); - // The mouse position relative to the node - // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner + // Find the normalized cursor position relative to the node. + // (±0., 0.) is the center with the corners at points (±0.5, ±0.5). // Coordinates are relative to the entire node, not just the visible region. for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) { - let relative_cursor_position = (*cursor_position - node_rect.min) / node_rect.size(); - - if visible_rect - .normalize(node_rect) - .contains(relative_cursor_position) - && pick_rounded_rect( - *cursor_position - node_rect.center(), - node_rect.size(), - node.node.border_radius, + if node.node.contains_point(*node.transform, *cursor_position) + && clip_check_recursive( + *cursor_position, + *node_entity, + &clipping_query, + &child_of_query, ) { hit_nodes .entry((camera_entity, *pointer_id)) .or_default() - .push((*node_entity, relative_cursor_position)); + .push(( + *node_entity, + node.transform.inverse().transform_point2(*cursor_position) + / node.node.size(), + )); } } } @@ -262,3 +252,27 @@ pub fn ui_picking( output.write(PointerHits::new(*pointer, picks, order)); } } + +/// Walk up the tree child-to-parent checking that `point` is not clipped by any ancestor node. +pub fn clip_check_recursive( + point: Vec2, + entity: Entity, + clipping_query: &Query<'_, '_, (&ComputedNode, &UiGlobalTransform, &Node)>, + child_of_query: &Query<&ChildOf>, +) -> bool { + if let Ok(child_of) = child_of_query.get(entity) { + let parent = child_of.0; + if let Ok((computed_node, transform, node)) = clipping_query.get(parent) { + if !computed_node + .resolve_clip_rect(node.overflow, node.overflow_clip_margin) + .contains(transform.inverse().transform_point2(point)) + { + // The point is clipped and should be ignored by picking + return false; + } + } + return clip_check_recursive(point, parent, clipping_query, child_of_query); + } + // Reached root, point unclipped by all ancestors + true +} diff --git a/crates/bevy_ui/src/render/box_shadow.rs b/crates/bevy_ui/src/render/box_shadow.rs index 1c2b2c7d0a..b6f3f3501e 100644 --- a/crates/bevy_ui/src/render/box_shadow.rs +++ b/crates/bevy_ui/src/render/box_shadow.rs @@ -2,6 +2,7 @@ use core::{hash::Hash, ops::Range}; +use crate::prelude::UiGlobalTransform; use crate::{ BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystems, ResolvedBorderRadius, TransparentUi, Val, @@ -18,7 +19,7 @@ use bevy_ecs::{ }, }; use bevy_image::BevyDefault as _; -use bevy_math::{vec2, FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles}; +use bevy_math::{vec2, Affine2, FloatOrd, Rect, Vec2}; use bevy_render::sync_world::MainEntity; use bevy_render::RenderApp; use bevy_render::{ @@ -29,7 +30,6 @@ use bevy_render::{ view::*, Extract, ExtractSchedule, Render, RenderSystems, }; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; use super::{stack_z_offsets, UiCameraMap, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS}; @@ -211,7 +211,7 @@ impl SpecializedRenderPipeline for BoxShadowPipeline { /// Description of a shadow to be sorted and queued for rendering pub struct ExtractedBoxShadow { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub bounds: Vec2, pub clip: Option, pub extracted_camera_entity: Entity, @@ -236,7 +236,7 @@ pub fn extract_shadows( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, &BoxShadow, Option<&CalculatedClip>, @@ -302,7 +302,7 @@ pub fn extract_shadows( extracted_box_shadows.box_shadows.push(ExtractedBoxShadow { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix() * Mat4::from_translation(offset.extend(0.)), + transform: Affine2::from(transform) * Affine2::from_translation(offset), color: drop_shadow.color.into(), bounds: shadow_size + 6. * blur_radius, clip: clip.map(|clip| clip.clip), @@ -405,11 +405,15 @@ pub fn prepare_shadows( .get(item.index) .filter(|n| item.entity() == n.render_entity) { - let rect_size = box_shadow.bounds.extend(1.0); + let rect_size = box_shadow.bounds; // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (box_shadow.transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + box_shadow + .transform + .transform_point2(pos * rect_size) + .extend(0.) + }); // 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) @@ -443,7 +447,7 @@ pub fn prepare_shadows( positions[3] + positions_diff[3].extend(0.), ]; - let transformed_rect_size = box_shadow.transform.transform_vector3(rect_size); + let transformed_rect_size = box_shadow.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -492,7 +496,7 @@ pub fn prepare_shadows( size: box_shadow.size.into(), radius, blur: box_shadow.blur_radius, - bounds: rect_size.xy().into(), + bounds: rect_size.into(), }); } diff --git a/crates/bevy_ui/src/render/debug_overlay.rs b/crates/bevy_ui/src/render/debug_overlay.rs index aa9440b8d8..c3ada22c2e 100644 --- a/crates/bevy_ui/src/render/debug_overlay.rs +++ b/crates/bevy_ui/src/render/debug_overlay.rs @@ -1,5 +1,6 @@ use crate::shader_flags; use crate::ui_node::ComputedNodeTarget; +use crate::ui_transform::UiGlobalTransform; use crate::CalculatedClip; use crate::ComputedNode; use bevy_asset::AssetId; @@ -16,7 +17,6 @@ use bevy_render::sync_world::TemporaryRenderEntity; use bevy_render::view::InheritedVisibility; use bevy_render::Extract; use bevy_sprite::BorderRect; -use bevy_transform::components::GlobalTransform; use super::ExtractedUiItem; use super::ExtractedUiNode; @@ -62,9 +62,9 @@ pub fn extract_debug_overlay( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, - &GlobalTransform, &ComputedNodeTarget, )>, >, @@ -76,7 +76,7 @@ pub fn extract_debug_overlay( let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, visibility, maybe_clip, transform, computed_target) in &uinode_query { + for (entity, uinode, transform, visibility, maybe_clip, computed_target) in &uinode_query { if !debug_options.show_hidden && !visibility.get() { continue; } @@ -102,7 +102,7 @@ pub fn extract_debug_overlay( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), diff --git a/crates/bevy_ui/src/render/gradient.rs b/crates/bevy_ui/src/render/gradient.rs index 31369899f7..bd818c7d5b 100644 --- a/crates/bevy_ui/src/render/gradient.rs +++ b/crates/bevy_ui/src/render/gradient.rs @@ -17,8 +17,9 @@ use bevy_ecs::{ use bevy_image::prelude::*; use bevy_math::{ ops::{cos, sin}, - FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles, + FloatOrd, Rect, Vec2, }; +use bevy_math::{Affine2, Vec2Swizzles}; use bevy_render::sync_world::MainEntity; use bevy_render::{ render_phase::*, @@ -29,7 +30,6 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::BorderRect; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; use super::shader_flags::BORDER_ALL; @@ -238,7 +238,7 @@ pub enum ResolvedGradient { pub struct ExtractedGradient { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub clip: Option, pub extracted_camera_entity: Entity, @@ -354,7 +354,7 @@ pub fn extract_gradients( Entity, &ComputedNode, &ComputedNodeTarget, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, AnyOf<(&BackgroundGradient, &BorderGradient)>, @@ -414,7 +414,7 @@ pub fn extract_gradients( border_radius: uinode.border_radius, border: uinode.border, node_type, - transform: transform.compute_matrix(), + transform: transform.into(), }, main_entity: entity.into(), render_entity: commands.spawn(TemporaryRenderEntity).id(), @@ -439,7 +439,7 @@ pub fn extract_gradients( extracted_gradients.items.push(ExtractedGradient { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), stops_range: range_start..extracted_color_stops.0.len(), rect: Rect { min: Vec2::ZERO, @@ -487,7 +487,7 @@ pub fn extract_gradients( extracted_gradients.items.push(ExtractedGradient { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), stops_range: range_start..extracted_color_stops.0.len(), rect: Rect { min: Vec2::ZERO, @@ -541,7 +541,7 @@ pub fn extract_gradients( extracted_gradients.items.push(ExtractedGradient { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), stops_range: range_start..extracted_color_stops.0.len(), rect: Rect { min: Vec2::ZERO, @@ -675,12 +675,16 @@ pub fn prepare_gradient( *item.batch_range_mut() = item_index as u32..item_index as u32 + 1; let uinode_rect = gradient.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (gradient.transform * (pos * rect_size).extend(1.)).xyz()); - let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + gradient + .transform + .transform_point2(pos * rect_size) + .extend(0.) + }); + let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size); // 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) @@ -721,7 +725,7 @@ pub fn prepare_gradient( corner_points[3] + positions_diff[3], ]; - let transformed_rect_size = gradient.transform.transform_vector3(rect_size); + let transformed_rect_size = gradient.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 83140d0f3b..61319eda9b 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -8,7 +8,9 @@ pub mod ui_texture_slice_pipeline; mod debug_overlay; mod gradient; +use crate::prelude::UiGlobalTransform; use crate::widget::{ImageNode, ViewportNode}; + use crate::{ BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias, @@ -22,7 +24,7 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_ecs::prelude::*; use bevy_ecs::system::SystemParam; use bevy_image::prelude::*; -use bevy_math::{FloatOrd, Mat4, Rect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Mat4, Rect, UVec4, Vec2}; use bevy_render::load_shader_library; use bevy_render::render_graph::{NodeRunError, RenderGraphContext}; use bevy_render::render_phase::ViewSortedRenderPhases; @@ -243,7 +245,7 @@ pub enum ExtractedUiItem { /// Ordering: left, top, right, bottom. border: BorderRect, node_type: NodeType, - transform: Mat4, + transform: Affine2, }, /// A contiguous sequence of text glyphs from the same section Glyphs { @@ -253,7 +255,7 @@ pub enum ExtractedUiItem { } pub struct ExtractedGlyph { - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, } @@ -344,7 +346,7 @@ pub fn extract_uinode_background_colors( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -383,7 +385,7 @@ pub fn extract_uinode_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: uinode.border(), @@ -403,7 +405,7 @@ pub fn extract_uinode_images( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -467,7 +469,7 @@ pub fn extract_uinode_images( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: image.flip_x, flip_y: image.flip_y, border: uinode.border, @@ -487,7 +489,7 @@ pub fn extract_uinode_borders( Entity, &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -503,7 +505,7 @@ pub fn extract_uinode_borders( entity, node, computed_node, - global_transform, + transform, inherited_visibility, maybe_clip, camera, @@ -567,7 +569,7 @@ pub fn extract_uinode_borders( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: global_transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: computed_node.border(), @@ -600,7 +602,7 @@ pub fn extract_uinode_borders( clip: maybe_clip.map(|clip| clip.clip), extracted_camera_entity, item: ExtractedUiItem::Node { - transform: global_transform.compute_matrix(), + transform: transform.into(), atlas_scaling: None, flip_x: false, flip_y: false, @@ -749,7 +751,7 @@ pub fn extract_viewport_nodes( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -792,7 +794,7 @@ pub fn extract_viewport_nodes( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: uinode.border(), @@ -812,7 +814,7 @@ pub fn extract_text_sections( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -830,7 +832,7 @@ pub fn extract_text_sections( for ( entity, uinode, - global_transform, + transform, inherited_visibility, clip, camera, @@ -847,8 +849,7 @@ pub fn extract_text_sections( continue; }; - let transform = global_transform.affine() - * bevy_math::Affine3A::from_translation((-0.5 * uinode.size()).extend(0.)); + let transform = Affine2::from(*transform) * Affine2::from_translation(-0.5 * uinode.size()); for ( i, @@ -866,7 +867,7 @@ pub fn extract_text_sections( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Mat4::from_translation(position.extend(0.)), + transform: transform * Affine2::from_translation(*position), rect, }); @@ -910,8 +911,8 @@ pub fn extract_text_shadows( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &ComputedNodeTarget, - &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &TextLayoutInfo, @@ -924,16 +925,8 @@ pub fn extract_text_shadows( let mut end = start + 1; let mut camera_mapper = camera_map.get_mapper(); - for ( - entity, - uinode, - target, - global_transform, - inherited_visibility, - clip, - text_layout_info, - shadow, - ) in &uinode_query + for (entity, uinode, transform, target, inherited_visibility, clip, text_layout_info, shadow) in + &uinode_query { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) if !inherited_visibility.get() || uinode.is_empty() { @@ -944,9 +937,9 @@ pub fn extract_text_shadows( continue; }; - let transform = global_transform.affine() - * Mat4::from_translation( - (-0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor()).extend(0.), + let node_transform = Affine2::from(*transform) + * Affine2::from_translation( + -0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor(), ); for ( @@ -965,7 +958,7 @@ pub fn extract_text_shadows( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Mat4::from_translation(position.extend(0.)), + transform: node_transform * Affine2::from_translation(*position), rect, }); @@ -998,7 +991,7 @@ pub fn extract_text_background_colors( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -1021,8 +1014,8 @@ pub fn extract_text_background_colors( continue; }; - let transform = global_transform.affine() - * bevy_math::Affine3A::from_translation(-0.5 * uinode.size().extend(0.)); + let transform = + Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); for &(section_entity, rect) in text_layout_info.section_rects.iter() { let Ok(text_background_color) = text_background_colors_query.get(section_entity) else { @@ -1042,7 +1035,7 @@ pub fn extract_text_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform * Mat4::from_translation(rect.center().extend(0.)), + transform: transform * Affine2::from_translation(rect.center()), flip_x: false, flip_y: false, border: uinode.border(), @@ -1093,11 +1086,11 @@ impl Default for UiMeta { } } -pub(crate) const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ - Vec3::new(-0.5, -0.5, 0.0), - Vec3::new(0.5, -0.5, 0.0), - Vec3::new(0.5, 0.5, 0.0), - Vec3::new(-0.5, 0.5, 0.0), +pub(crate) const QUAD_VERTEX_POSITIONS: [Vec2; 4] = [ + Vec2::new(-0.5, -0.5), + Vec2::new(0.5, -0.5), + Vec2::new(0.5, 0.5), + Vec2::new(-0.5, 0.5), ]; pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; @@ -1321,12 +1314,12 @@ pub fn prepare_uinodes( let mut uinode_rect = extracted_uinode.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (*transform * (pos * rect_size).extend(1.)).xyz()); - let points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); + .map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); + let points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size); // 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) @@ -1367,7 +1360,7 @@ pub fn prepare_uinodes( points[3] + positions_diff[3], ]; - let transformed_rect_size = transform.transform_vector3(rect_size); + let transformed_rect_size = transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -1448,7 +1441,7 @@ pub fn prepare_uinodes( border_radius.bottom_left, ], border: [border.left, border.top, border.right, border.bottom], - size: rect_size.xy().into(), + size: rect_size.into(), point: points[i].into(), }); } @@ -1470,13 +1463,14 @@ pub fn prepare_uinodes( let color = extracted_uinode.color.to_f32_array(); for glyph in &extracted_uinodes.glyphs[range.clone()] { let glyph_rect = glyph.rect; - let size = glyph.rect.size(); - - let rect_size = glyph_rect.size().extend(1.0); + let rect_size = glyph_rect.size(); // Specify the corners of the glyph let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (glyph.transform * (pos * rect_size).extend(1.)).xyz() + glyph + .transform + .transform_point2(pos * glyph_rect.size()) + .extend(0.) }); let positions_diff = if let Some(clip) = extracted_uinode.clip { @@ -1511,7 +1505,7 @@ pub fn prepare_uinodes( // cull nodes that are completely clipped let transformed_rect_size = - glyph.transform.transform_vector3(rect_size); + glyph.transform.transform_vector2(rect_size); if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x.abs() || positions_diff[1].y - positions_diff[2].y @@ -1548,7 +1542,7 @@ pub fn prepare_uinodes( flags: shader_flags::TEXTURED | shader_flags::CORNERS[i], radius: [0.0; 4], border: [0.0; 4], - size: size.into(), + size: rect_size.into(), point: [0.0; 2], }); } diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 02fab4fdee..ebdeacccf9 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -1,9 +1,7 @@ -use core::{hash::Hash, marker::PhantomData, ops::Range}; - use crate::*; use bevy_asset::*; use bevy_ecs::{ - prelude::Component, + prelude::{Component, With}, query::ROQueryItem, system::{ lifetimeless::{Read, SRes}, @@ -11,24 +9,22 @@ use bevy_ecs::{ }, }; use bevy_image::BevyDefault as _; -use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_render::{ extract_component::ExtractComponentPlugin, globals::{GlobalsBuffer, GlobalsUniform}, + load_shader_library, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, render_phase::*, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderDevice, RenderQueue}, + sync_world::{MainEntity, TemporaryRenderEntity}, view::*, Extract, ExtractSchedule, Render, RenderSystems, }; -use bevy_render::{ - load_shader_library, - sync_world::{MainEntity, TemporaryRenderEntity}, -}; use bevy_sprite::BorderRect; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; +use core::{hash::Hash, marker::PhantomData, ops::Range}; /// Adds the necessary ECS resources and render logic to enable rendering entities using the given /// [`UiMaterial`] asset type (which includes [`UiMaterial`] types). @@ -321,7 +317,7 @@ impl RenderCommand

for DrawUiMaterialNode { pub struct ExtractedUiMaterialNode { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub border: BorderRect, pub border_radius: ResolvedBorderRadius, @@ -356,7 +352,7 @@ pub fn extract_ui_material_nodes( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &MaterialNode, &InheritedVisibility, Option<&CalculatedClip>, @@ -387,7 +383,7 @@ pub fn extract_ui_material_nodes( extracted_uinodes.uinodes.push(ExtractedUiMaterialNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: computed_node.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), material: handle.id(), rect: Rect { min: Vec2::ZERO, @@ -459,10 +455,13 @@ pub fn prepare_uimaterial_nodes( let uinode_rect = extracted_uinode.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (extracted_uinode.transform * (pos * rect_size).extend(1.0)).xyz() + extracted_uinode + .transform + .transform_point2(pos * rect_size) + .extend(1.0) }); let positions_diff = if let Some(clip) = extracted_uinode.clip { @@ -496,7 +495,7 @@ pub fn prepare_uimaterial_nodes( ]; let transformed_rect_size = - extracted_uinode.transform.transform_vector3(rect_size); + extracted_uinode.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π 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 80a55bbcd4..0e232ab1cc 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -1,5 +1,6 @@ use core::{hash::Hash, ops::Range}; +use crate::prelude::UiGlobalTransform; use crate::*; use bevy_asset::*; use bevy_color::{Alpha, ColorToComponents, LinearRgba}; @@ -11,7 +12,7 @@ use bevy_ecs::{ }, }; use bevy_image::prelude::*; -use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_platform::collections::HashMap; use bevy_render::sync_world::MainEntity; use bevy_render::{ @@ -25,7 +26,6 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::{SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureSlicer}; -use bevy_transform::prelude::GlobalTransform; use binding_types::{sampler, texture_2d}; use bytemuck::{Pod, Zeroable}; use widget::ImageNode; @@ -218,7 +218,7 @@ impl SpecializedRenderPipeline for UiTextureSlicePipeline { pub struct ExtractedUiTextureSlice { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub atlas_rect: Option, pub image: AssetId, @@ -246,7 +246,7 @@ pub fn extract_ui_texture_slices( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -306,7 +306,7 @@ pub fn extract_ui_texture_slices( extracted_ui_slicers.slices.push(ExtractedUiTextureSlice { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), color: image.color.into(), rect: Rect { min: Vec2::ZERO, @@ -497,11 +497,12 @@ pub fn prepare_ui_slices( let uinode_rect = texture_slices.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // 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.transform_point2(pos * rect_size)).extend(0.) + }); // 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) @@ -536,7 +537,7 @@ pub fn prepare_ui_slices( ]; let transformed_rect_size = - texture_slices.transform.transform_vector3(rect_size); + texture_slices.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index f5f914bdc0..343d8af3cd 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1,4 +1,7 @@ -use crate::{FocusPolicy, UiRect, Val}; +use crate::{ + ui_transform::{UiGlobalTransform, UiTransform}, + FocusPolicy, UiRect, Val, +}; use bevy_color::{Alpha, Color}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, system::SystemParam}; @@ -9,7 +12,6 @@ use bevy_render::{ view::Visibility, }; use bevy_sprite::BorderRect; -use bevy_transform::components::Transform; use bevy_utils::once; use bevy_window::{PrimaryWindow, WindowRef}; use core::{f32, num::NonZero}; @@ -229,6 +231,73 @@ impl ComputedNode { pub const fn inverse_scale_factor(&self) -> f32 { self.inverse_scale_factor } + + // Returns true if `point` within the node. + // + // Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles. + pub fn contains_point(&self, transform: UiGlobalTransform, point: Vec2) -> bool { + let Some(local_point) = transform + .try_inverse() + .map(|transform| transform.transform_point2(point)) + else { + return false; + }; + let [top, bottom] = if local_point.x < 0. { + [self.border_radius.top_left, self.border_radius.bottom_left] + } else { + [ + self.border_radius.top_right, + self.border_radius.bottom_right, + ] + }; + let r = if local_point.y < 0. { top } else { bottom }; + let corner_to_point = local_point.abs() - 0.5 * self.size; + let q = corner_to_point + r; + let l = q.max(Vec2::ZERO).length(); + let m = q.max_element().min(0.); + l + m - r < 0. + } + + /// Transform a point to normalized node space with the center of the node at the origin and the corners at [+/-0.5, +/-0.5] + pub fn normalize_point(&self, transform: UiGlobalTransform, point: Vec2) -> Option { + self.size + .cmpgt(Vec2::ZERO) + .all() + .then(|| transform.try_inverse()) + .flatten() + .map(|transform| transform.transform_point2(point) / self.size) + } + + /// Resolve the node's clipping rect in local space + pub fn resolve_clip_rect( + &self, + overflow: Overflow, + overflow_clip_margin: OverflowClipMargin, + ) -> Rect { + let mut clip_rect = Rect::from_center_size(Vec2::ZERO, self.size); + + let clip_inset = match overflow_clip_margin.visual_box { + OverflowClipBox::BorderBox => BorderRect::ZERO, + OverflowClipBox::ContentBox => self.content_inset(), + OverflowClipBox::PaddingBox => self.border(), + }; + + clip_rect.min.x += clip_inset.left; + clip_rect.min.y += clip_inset.top; + clip_rect.max.x -= clip_inset.right; + clip_rect.max.y -= clip_inset.bottom; + + if overflow.x == OverflowAxis::Visible { + clip_rect.min.x = -f32::INFINITY; + clip_rect.max.x = f32::INFINITY; + } + if overflow.y == OverflowAxis::Visible { + clip_rect.min.y = -f32::INFINITY; + clip_rect.max.y = f32::INFINITY; + } + + clip_rect + } } impl ComputedNode { @@ -323,12 +392,12 @@ impl From for ScrollPosition { #[require( ComputedNode, ComputedNodeTarget, + UiTransform, BackgroundColor, BorderColor, BorderRadius, FocusPolicy, ScrollPosition, - Transform, Visibility, ZIndex )] diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs new file mode 100644 index 0000000000..47f8484e54 --- /dev/null +++ b/crates/bevy_ui/src/ui_transform.rs @@ -0,0 +1,191 @@ +use crate::Val; +use bevy_derive::Deref; +use bevy_ecs::component::Component; +use bevy_ecs::prelude::ReflectComponent; +use bevy_math::Affine2; +use bevy_math::Rot2; +use bevy_math::Vec2; +use bevy_reflect::prelude::*; + +/// A pair of [`Val`]s used to represent a 2-dimensional size or offset. +#[derive(Debug, PartialEq, Clone, Copy, Reflect)] +#[reflect(Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct Val2 { + /// Translate the node along the x-axis. + /// `Val::Percent` values are resolved based on the computed width of the Ui Node. + /// `Val::Auto` is resolved to `0.`. + pub x: Val, + /// Translate the node along the y-axis. + /// `Val::Percent` values are resolved based on the computed height of the UI Node. + /// `Val::Auto` is resolved to `0.`. + pub y: Val, +} + +impl Val2 { + pub const ZERO: Self = Self { + x: Val::ZERO, + y: Val::ZERO, + }; + + /// Creates a new [`Val2`] where both components are in logical pixels + pub const fn px(x: f32, y: f32) -> Self { + Self { + x: Val::Px(x), + y: Val::Px(y), + } + } + + /// Creates a new [`Val2`] where both components are percentage values + pub const fn percent(x: f32, y: f32) -> Self { + Self { + x: Val::Percent(x), + y: Val::Percent(y), + } + } + + /// Creates a new [`Val2`] + pub const fn new(x: Val, y: Val) -> Self { + Self { x, y } + } + + /// Resolves this [`Val2`] from the given `scale_factor`, `parent_size`, + /// and `viewport_size`. + /// + /// Component values of [`Val::Auto`] are resolved to 0. + pub fn resolve(&self, scale_factor: f32, base_size: Vec2, viewport_size: Vec2) -> Vec2 { + Vec2::new( + self.x + .resolve(scale_factor, base_size.x, viewport_size) + .unwrap_or(0.), + self.y + .resolve(scale_factor, base_size.y, viewport_size) + .unwrap_or(0.), + ) + } +} + +impl Default for Val2 { + fn default() -> Self { + Self::ZERO + } +} + +/// Relative 2D transform for UI nodes +/// +/// [`UiGlobalTransform`] is automatically inserted whenever [`UiTransform`] is inserted. +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[require(UiGlobalTransform)] +pub struct UiTransform { + /// Translate the node. + pub translation: Val2, + /// Scale the node. A negative value reflects the node in that axis. + pub scale: Vec2, + /// Rotate the node clockwise. + pub rotation: Rot2, +} + +impl UiTransform { + pub const IDENTITY: Self = Self { + translation: Val2::ZERO, + scale: Vec2::ONE, + rotation: Rot2::IDENTITY, + }; + + /// Creates a UI transform representing a rotation. + pub fn from_rotation(rotation: Rot2) -> Self { + Self { + rotation, + ..Self::IDENTITY + } + } + + /// Creates a UI transform representing a responsive translation. + pub fn from_translation(translation: Val2) -> Self { + Self { + translation, + ..Self::IDENTITY + } + } + + /// Creates a UI transform representing a scaling. + pub fn from_scale(scale: Vec2) -> Self { + Self { + scale, + ..Self::IDENTITY + } + } + + /// Resolves the translation from the given `scale_factor`, `base_value`, and `target_size` + /// and returns a 2d affine transform from the resolved translation, and the `UiTransform`'s rotation, and scale. + pub fn compute_affine(&self, scale_factor: f32, base_size: Vec2, target_size: Vec2) -> Affine2 { + Affine2::from_scale_angle_translation( + self.scale, + self.rotation.as_radians(), + self.translation + .resolve(scale_factor, base_size, target_size), + ) + } +} + +impl Default for UiTransform { + fn default() -> Self { + Self::IDENTITY + } +} + +/// Absolute 2D transform for UI nodes +/// +/// [`UiGlobalTransform`]s are updated from [`UiTransform`] and [`Node`](crate::ui_node::Node) +/// in [`ui_layout_system`](crate::layout::ui_layout_system) +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect, Deref)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct UiGlobalTransform(Affine2); + +impl Default for UiGlobalTransform { + fn default() -> Self { + Self(Affine2::IDENTITY) + } +} + +impl UiGlobalTransform { + /// If the transform is invertible returns its inverse. + /// Otherwise returns `None`. + #[inline] + pub fn try_inverse(&self) -> Option { + (self.matrix2.determinant() != 0.).then_some(self.inverse()) + } +} + +impl From for UiGlobalTransform { + fn from(value: Affine2) -> Self { + Self(value) + } +} + +impl From for Affine2 { + fn from(value: UiGlobalTransform) -> Self { + value.0 + } +} + +impl From<&UiGlobalTransform> for Affine2 { + fn from(value: &UiGlobalTransform) -> Self { + value.0 + } +} diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 7e27c4abdd..c0e9d09d7b 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -2,6 +2,7 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, + ui_transform::UiGlobalTransform, CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, UiScale, UiTargetCamera, }; @@ -17,7 +18,6 @@ use bevy_ecs::{ use bevy_math::{Rect, UVec2}; use bevy_render::camera::Camera; use bevy_sprite::BorderRect; -use bevy_transform::components::GlobalTransform; /// Updates clipping for all nodes pub fn update_clipping_system( @@ -26,7 +26,7 @@ pub fn update_clipping_system( mut node_query: Query<( &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, Option<&mut CalculatedClip>, )>, ui_children: UiChildren, @@ -48,14 +48,13 @@ fn update_clipping( node_query: &mut Query<( &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, Option<&mut CalculatedClip>, )>, entity: Entity, mut maybe_inherited_clip: Option, ) { - let Ok((node, computed_node, global_transform, maybe_calculated_clip)) = - node_query.get_mut(entity) + let Ok((node, computed_node, transform, maybe_calculated_clip)) = node_query.get_mut(entity) else { return; }; @@ -91,10 +90,7 @@ fn update_clipping( maybe_inherited_clip } else { // Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists - let mut clip_rect = Rect::from_center_size( - global_transform.translation().truncate(), - computed_node.size(), - ); + let mut clip_rect = Rect::from_center_size(transform.translation, computed_node.size()); // Content isn't clipped at the edges of the node but at the edges of the region specified by [`Node::overflow_clip_margin`]. // diff --git a/examples/README.md b/examples/README.md index 37c1ae4621..dce7c114e8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -571,6 +571,7 @@ Example | Description [UI Texture Atlas Slice](../examples/ui/ui_texture_atlas_slice.rs) | Illustrates how to use 9 Slicing for TextureAtlases in UI [UI Texture Slice](../examples/ui/ui_texture_slice.rs) | Illustrates how to use 9 Slicing in UI [UI Texture Slice Flipping and Tiling](../examples/ui/ui_texture_slice_flip_and_tile.rs) | Illustrates how to flip and tile images with 9 Slicing in UI +[UI Transform](../examples/ui/ui_transform.rs) | An example demonstrating how to translate, rotate and scale UI elements. [UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements [Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates [Viewport Node](../examples/ui/viewport_node.rs) | Demonstrates how to create a viewport node with picking support diff --git a/examples/testbed/full_ui.rs b/examples/testbed/full_ui.rs index 02f69ce955..634945f057 100644 --- a/examples/testbed/full_ui.rs +++ b/examples/testbed/full_ui.rs @@ -228,7 +228,13 @@ fn setup(mut commands: Commands, asset_server: Res) { parent.spawn(( ImageNode::new(asset_server.load("branding/bevy_logo_light.png")), // Uses the transform to rotate the logo image by 45 degrees - Transform::from_rotation(Quat::from_rotation_z(0.25 * PI)), + Node { + ..Default::default() + }, + UiTransform { + rotation: Rot2::radians(0.25 * PI), + ..Default::default() + }, BorderRadius::all(Val::Px(10.)), Outline { width: Val::Px(2.), diff --git a/examples/ui/overflow_debug.rs b/examples/ui/overflow_debug.rs index 400c257166..7f77cb98c6 100644 --- a/examples/ui/overflow_debug.rs +++ b/examples/ui/overflow_debug.rs @@ -4,7 +4,6 @@ use bevy::{input::common_conditions::input_just_pressed, prelude::*, ui::widget: use std::f32::consts::{FRAC_PI_2, PI, TAU}; const CONTAINER_SIZE: f32 = 150.0; -const HALF_CONTAINER_SIZE: f32 = CONTAINER_SIZE / 2.0; const LOOP_LENGTH: f32 = 4.0; fn main() { @@ -41,16 +40,16 @@ struct AnimationState { struct Container(u8); trait UpdateTransform { - fn update(&self, t: f32, transform: &mut Transform); + fn update(&self, t: f32, transform: &mut UiTransform); } #[derive(Component)] struct Move; impl UpdateTransform for Move { - fn update(&self, t: f32, transform: &mut Transform) { - transform.translation.x = ops::sin(t * TAU - FRAC_PI_2) * HALF_CONTAINER_SIZE; - transform.translation.y = -ops::cos(t * TAU - FRAC_PI_2) * HALF_CONTAINER_SIZE; + fn update(&self, t: f32, transform: &mut UiTransform) { + transform.translation.x = Val::Percent(ops::sin(t * TAU - FRAC_PI_2) * 50.); + transform.translation.y = Val::Percent(-ops::cos(t * TAU - FRAC_PI_2) * 50.); } } @@ -58,7 +57,7 @@ impl UpdateTransform for Move { struct Scale; impl UpdateTransform for Scale { - fn update(&self, t: f32, transform: &mut Transform) { + fn update(&self, t: f32, transform: &mut UiTransform) { transform.scale.x = 1.0 + 0.5 * ops::cos(t * TAU).max(0.0); transform.scale.y = 1.0 + 0.5 * ops::cos(t * TAU + PI).max(0.0); } @@ -68,9 +67,8 @@ impl UpdateTransform for Scale { struct Rotate; impl UpdateTransform for Rotate { - fn update(&self, t: f32, transform: &mut Transform) { - transform.rotation = - Quat::from_axis_angle(Vec3::Z, (ops::cos(t * TAU) * 45.0).to_radians()); + fn update(&self, t: f32, transform: &mut UiTransform) { + transform.rotation = Rot2::radians(ops::cos(t * TAU) * 45.0); } } @@ -175,10 +173,6 @@ fn spawn_container( update_transform: impl UpdateTransform + Component, spawn_children: impl FnOnce(&mut ChildSpawnerCommands), ) { - let mut transform = Transform::default(); - - update_transform.update(0.0, &mut transform); - parent .spawn(( Node { @@ -198,11 +192,8 @@ fn spawn_container( Node { align_items: AlignItems::Center, justify_content: JustifyContent::Center, - top: Val::Px(transform.translation.x), - left: Val::Px(transform.translation.y), ..default() }, - transform, update_transform, )) .with_children(spawn_children); @@ -233,13 +224,10 @@ fn update_animation( fn update_transform( animation: Res, - mut containers: Query<(&mut Transform, &mut Node, &ComputedNode, &T)>, + mut containers: Query<(&mut UiTransform, &T)>, ) { - for (mut transform, mut node, computed_node, update_transform) in &mut containers { + for (mut transform, update_transform) in &mut containers { update_transform.update(animation.t, &mut transform); - - node.left = Val::Px(transform.translation.x * computed_node.inverse_scale_factor()); - node.top = Val::Px(transform.translation.y * computed_node.inverse_scale_factor()); } } diff --git a/examples/ui/relative_cursor_position.rs b/examples/ui/relative_cursor_position.rs index 796e810895..5346918257 100644 --- a/examples/ui/relative_cursor_position.rs +++ b/examples/ui/relative_cursor_position.rs @@ -78,7 +78,7 @@ fn relative_cursor_position_system( "unknown".to_string() }; - text_color.0 = if relative_cursor_position.mouse_over() { + text_color.0 = if relative_cursor_position.cursor_over() { Color::srgb(0.1, 0.9, 0.1) } else { Color::srgb(0.9, 0.1, 0.1) diff --git a/examples/ui/ui_transform.rs b/examples/ui/ui_transform.rs new file mode 100644 index 0000000000..68abd0ba36 --- /dev/null +++ b/examples/ui/ui_transform.rs @@ -0,0 +1,302 @@ +//! An example demonstrating how to translate, rotate and scale UI elements. +use bevy::color::palettes::css::DARK_GRAY; +use bevy::color::palettes::css::RED; +use bevy::color::palettes::css::YELLOW; +use bevy::prelude::*; +use core::f32::consts::FRAC_PI_8; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, button_system) + .add_systems(Update, translation_system) + .run(); +} + +const NORMAL_BUTTON: Color = Color::WHITE; +const HOVERED_BUTTON: Color = Color::Srgba(YELLOW); +const PRESSED_BUTTON: Color = Color::Srgba(RED); + +/// A button that rotates the target node +#[derive(Component)] +pub struct RotateButton(pub Rot2); + +/// A button that scales the target node +#[derive(Component)] +pub struct ScaleButton(pub f32); + +/// Marker component so the systems know which entities to translate, rotate and scale +#[derive(Component)] +pub struct TargetNode; + +/// Handles button interactions +fn button_system( + mut interaction_query: Query< + ( + &Interaction, + &mut BackgroundColor, + Option<&RotateButton>, + Option<&ScaleButton>, + ), + (Changed, With