Optional ImageScaleMode (#11780)

> Follow up to #11600 and #10588 

@mockersf expressed some [valid
concerns](https://github.com/bevyengine/bevy/pull/11600#issuecomment-1932796498)
about the current system this PR attempts to fix:

The `ComputedTextureSlices` reacts to asset change in both `bevy_sprite`
and `bevy_ui`, meaning that if the `ImageScaleMode` is inserted by
default in the bundles, we will iterate through most 2d items every time
an asset is updated.

# Solution

- `ImageScaleMode` only has two variants: `Sliced` and `Tiled`. I
removed the `Stretched` default
- `ImageScaleMode` is no longer part of any bundle, but the relevant
bundles explain that this additional component can be inserted

This way, the *absence* of `ImageScaleMode` means the image will be
stretched, and its *presence* will include the entity to the various
slicing systems

Optional components in bundles would make this more straigthfoward

# Additional work

Should I add new bundles with the `ImageScaleMode` component ?
This commit is contained in:
Félix Lescaudey de Maneville 2024-02-09 21:36:32 +01:00 committed by GitHub
parent 0ebba278f7
commit e0c296ee14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 77 additions and 83 deletions

View File

@ -1,4 +1,4 @@
use crate::{texture_atlas::TextureAtlas, ImageScaleMode, Sprite}; use crate::{Sprite, TextureAtlas};
use bevy_asset::Handle; use bevy_asset::Handle;
use bevy_ecs::bundle::Bundle; use bevy_ecs::bundle::Bundle;
use bevy_render::{ use bevy_render::{
@ -8,12 +8,16 @@ use bevy_render::{
use bevy_transform::components::{GlobalTransform, Transform}; use bevy_transform::components::{GlobalTransform, Transform};
/// A [`Bundle`] of components for drawing a single sprite from an image. /// A [`Bundle`] of components for drawing a single sprite from an image.
///
/// # Extra behaviours
///
/// You may add the following components to enable additional behaviours
/// - [`ImageScaleMode`](crate::ImageScaleMode) to enable either slicing or tiling of the texture
/// - [`TextureAtlas`] to draw specific sections of a sprite sheet, (See [`SpriteSheetBundle`])
#[derive(Bundle, Clone, Default)] #[derive(Bundle, Clone, Default)]
pub struct SpriteBundle { pub struct SpriteBundle {
/// Specifies the rendering properties of the sprite, such as color tint and flip. /// Specifies the rendering properties of the sprite, such as color tint and flip.
pub sprite: Sprite, pub sprite: Sprite,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// The local transform of the sprite, relative to its parent. /// The local transform of the sprite, relative to its parent.
pub transform: Transform, pub transform: Transform,
/// The absolute transform of the sprite. This should generally not be written to directly. /// The absolute transform of the sprite. This should generally not be written to directly.
@ -41,8 +45,6 @@ pub struct SpriteBundle {
pub struct SpriteSheetBundle { pub struct SpriteSheetBundle {
/// Specifies the rendering properties of the sprite, such as color tint and flip. /// Specifies the rendering properties of the sprite, such as color tint and flip.
pub sprite: Sprite, pub sprite: Sprite,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// The local transform of the sprite, relative to its parent. /// The local transform of the sprite, relative to its parent.
pub transform: Transform, pub transform: Transform,
/// The absolute transform of the sprite. This should generally not be written to directly. /// The absolute transform of the sprite. This should generally not be written to directly.

View File

@ -32,12 +32,9 @@ pub struct Sprite {
} }
/// Controls how the image is altered when scaled. /// Controls how the image is altered when scaled.
#[derive(Component, Debug, Default, Clone, Reflect)] #[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default)] #[reflect(Component)]
pub enum ImageScaleMode { pub enum ImageScaleMode {
/// The entire texture stretches when its dimensions change. This is the default option.
#[default]
Stretched,
/// The texture will be cut in 9 slices, keeping the texture in proportions on resize /// The texture will be cut in 9 slices, keeping the texture in proportions on resize
Sliced(TextureSlicer), Sliced(TextureSlicer),
/// The texture will be repeated if stretched beyond `stretched_value` /// The texture will be repeated if stretched beyond `stretched_value`

View File

@ -8,7 +8,7 @@ use bevy_render::texture::Image;
use bevy_transform::prelude::*; use bevy_transform::prelude::*;
use bevy_utils::HashSet; use bevy_utils::HashSet;
/// Component storing texture slices for sprite entities with a tiled or sliced [`ImageScaleMode`] /// Component storing texture slices for sprite entities with a [`ImageScaleMode`]
/// ///
/// This component is automatically inserted and updated /// This component is automatically inserted and updated
#[derive(Debug, Clone, Component)] #[derive(Debug, Clone, Component)]
@ -62,9 +62,7 @@ impl ComputedTextureSlices {
/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices /// Generates sprite slices for a `sprite` given a `scale_mode`. The slices
/// will be computed according to the `image_handle` dimensions or the sprite rect. /// will be computed according to the `image_handle` dimensions or the sprite rect.
/// ///
/// Returns `None` if either: /// Returns `None` if the image asset is not loaded
/// - The scale mode is [`ImageScaleMode::Stretched`]
/// - The image asset is not loaded
#[must_use] #[must_use]
fn compute_sprite_slices( fn compute_sprite_slices(
sprite: &Sprite, sprite: &Sprite,
@ -72,9 +70,6 @@ fn compute_sprite_slices(
image_handle: &Handle<Image>, image_handle: &Handle<Image>,
images: &Assets<Image>, images: &Assets<Image>,
) -> Option<ComputedTextureSlices> { ) -> Option<ComputedTextureSlices> {
if let ImageScaleMode::Stretched = scale_mode {
return None;
}
let image_size = images.get(image_handle).map(|i| { let image_size = images.get(image_handle).map(|i| {
Vec2::new( Vec2::new(
i.texture_descriptor.size.width as f32, i.texture_descriptor.size.width as f32,
@ -82,7 +77,6 @@ fn compute_sprite_slices(
) )
})?; })?;
let slices = match scale_mode { let slices = match scale_mode {
ImageScaleMode::Stretched => unreachable!(),
ImageScaleMode::Sliced(slicer) => slicer.compute_slices( ImageScaleMode::Sliced(slicer) => slicer.compute_slices(
sprite.rect.unwrap_or(Rect { sprite.rect.unwrap_or(Rect {
min: Vec2::ZERO, min: Vec2::ZERO,
@ -110,7 +104,7 @@ fn compute_sprite_slices(
} }
/// System reacting to added or modified [`Image`] handles, and recompute sprite slices /// System reacting to added or modified [`Image`] handles, and recompute sprite slices
/// on matching sprite entities /// on matching sprite entities with a [`ImageScaleMode`] component
pub(crate) fn compute_slices_on_asset_event( pub(crate) fn compute_slices_on_asset_event(
mut commands: Commands, mut commands: Commands,
mut events: EventReader<AssetEvent<Image>>, mut events: EventReader<AssetEvent<Image>>,
@ -140,6 +134,7 @@ pub(crate) fn compute_slices_on_asset_event(
} }
/// System reacting to changes on relevant sprite bundle components to compute the sprite slices /// System reacting to changes on relevant sprite bundle components to compute the sprite slices
/// on matching sprite entities with a [`ImageScaleMode`] component
pub(crate) fn compute_slices_on_sprite_change( pub(crate) fn compute_slices_on_sprite_change(
mut commands: Commands, mut commands: Commands,
images: Res<Assets<Image>>, images: Res<Assets<Image>>,

View File

@ -13,7 +13,7 @@ use bevy_render::{
prelude::Color, prelude::Color,
view::{InheritedVisibility, ViewVisibility, Visibility}, view::{InheritedVisibility, ViewVisibility, Visibility},
}; };
use bevy_sprite::{ImageScaleMode, TextureAtlas}; use bevy_sprite::TextureAtlas;
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
use bevy_text::{BreakLineOn, JustifyText, Text, TextLayoutInfo, TextSection, TextStyle}; use bevy_text::{BreakLineOn, JustifyText, Text, TextLayoutInfo, TextSection, TextStyle};
use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_transform::prelude::{GlobalTransform, Transform};
@ -76,6 +76,11 @@ impl Default for NodeBundle {
} }
/// A UI node that is an image /// A UI node that is an image
///
/// # Extra behaviours
///
/// You may add the following components to enable additional behaviours
/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture
#[derive(Bundle, Debug, Default)] #[derive(Bundle, Debug, Default)]
pub struct ImageBundle { pub struct ImageBundle {
/// Describes the logical size of the node /// Describes the logical size of the node
@ -95,8 +100,6 @@ pub struct ImageBundle {
/// ///
/// This component is set automatically /// This component is set automatically
pub image_size: UiImageSize, pub image_size: UiImageSize,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// Whether this node should block interaction with lower nodes /// Whether this node should block interaction with lower nodes
pub focus_policy: FocusPolicy, pub focus_policy: FocusPolicy,
/// The transform of the node /// The transform of the node
@ -288,6 +291,11 @@ where
} }
/// A UI node that is a button /// A UI node that is a button
///
/// # Extra behaviours
///
/// You may add the following components to enable additional behaviours
/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture
#[derive(Bundle, Clone, Debug)] #[derive(Bundle, Clone, Debug)]
pub struct ButtonBundle { pub struct ButtonBundle {
/// Describes the logical size of the node /// Describes the logical size of the node
@ -309,8 +317,6 @@ pub struct ButtonBundle {
pub border_color: BorderColor, pub border_color: BorderColor,
/// The image of the node /// The image of the node
pub image: UiImage, pub image: UiImage,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// The transform of the node /// The transform of the node
/// ///
/// This component is automatically managed by the UI layout system. /// This component is automatically managed by the UI layout system.
@ -347,7 +353,6 @@ impl Default for ButtonBundle {
inherited_visibility: Default::default(), inherited_visibility: Default::default(),
view_visibility: Default::default(), view_visibility: Default::default(),
z_index: Default::default(), z_index: Default::default(),
scale_mode: ImageScaleMode::default(),
} }
} }
} }

View File

@ -77,9 +77,7 @@ impl ComputedTextureSlices {
/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices /// Generates sprite slices for a `sprite` given a `scale_mode`. The slices
/// will be computed according to the `image_handle` dimensions or the sprite rect. /// will be computed according to the `image_handle` dimensions or the sprite rect.
/// ///
/// Returns `None` if either: /// Returns `None` if the image asset is not loaded
/// - The scale mode is [`ImageScaleMode::Stretched`]
/// - The image asset is not loaded
#[must_use] #[must_use]
fn compute_texture_slices( fn compute_texture_slices(
draw_area: Vec2, draw_area: Vec2,
@ -87,9 +85,6 @@ fn compute_texture_slices(
image_handle: &UiImage, image_handle: &UiImage,
images: &Assets<Image>, images: &Assets<Image>,
) -> Option<ComputedTextureSlices> { ) -> Option<ComputedTextureSlices> {
if let ImageScaleMode::Stretched = scale_mode {
return None;
}
let image_size = images.get(&image_handle.texture).map(|i| { let image_size = images.get(&image_handle.texture).map(|i| {
Vec2::new( Vec2::new(
i.texture_descriptor.size.width as f32, i.texture_descriptor.size.width as f32,
@ -101,7 +96,6 @@ fn compute_texture_slices(
max: image_size, max: image_size,
}; };
let slices = match scale_mode { let slices = match scale_mode {
ImageScaleMode::Stretched => unreachable!(),
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, Some(draw_area)), ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, Some(draw_area)),
ImageScaleMode::Tiled { ImageScaleMode::Tiled {
tile_x, tile_x,
@ -120,7 +114,7 @@ fn compute_texture_slices(
} }
/// System reacting to added or modified [`Image`] handles, and recompute sprite slices /// System reacting to added or modified [`Image`] handles, and recompute sprite slices
/// on matching sprite entities /// on matching sprite entities with a [`ImageScaleMode`] component
pub(crate) fn compute_slices_on_asset_event( pub(crate) fn compute_slices_on_asset_event(
mut commands: Commands, mut commands: Commands,
mut events: EventReader<AssetEvent<Image>>, mut events: EventReader<AssetEvent<Image>>,
@ -157,6 +151,7 @@ pub(crate) fn compute_slices_on_asset_event(
} }
/// System reacting to changes on relevant sprite bundle components to compute the sprite slices /// System reacting to changes on relevant sprite bundle components to compute the sprite slices
/// on matching sprite entities with a [`ImageScaleMode`] component
pub(crate) fn compute_slices_on_image_change( pub(crate) fn compute_slices_on_image_change(
mut commands: Commands, mut commands: Commands,
images: Res<Assets<Image>>, images: Res<Assets<Image>>,

View File

@ -25,89 +25,85 @@ fn spawn_sprites(
) { ) {
let cases = [ let cases = [
// Reference sprite // Reference sprite
( ("Original texture", style.clone(), Vec2::splat(100.0), None),
"Original texture",
style.clone(),
Vec2::splat(100.0),
ImageScaleMode::default(),
),
// Scaled regular sprite // Scaled regular sprite
( (
"Stretched texture", "Stretched texture",
style.clone(), style.clone(),
Vec2::new(100.0, 200.0), Vec2::new(100.0, 200.0),
ImageScaleMode::default(), None,
), ),
// Stretched Scaled sliced sprite // Stretched Scaled sliced sprite
( (
"Stretched and sliced", "Stretched and sliced",
style.clone(), style.clone(),
Vec2::new(100.0, 200.0), Vec2::new(100.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer { Some(ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border), border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Stretch, center_scale_mode: SliceScaleMode::Stretch,
..default() ..default()
}), })),
), ),
// Scaled sliced sprite // Scaled sliced sprite
( (
"Sliced and Tiled", "Sliced and Tiled",
style.clone(), style.clone(),
Vec2::new(100.0, 200.0), Vec2::new(100.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer { Some(ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border), border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.5 }, center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.5 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
..default() ..default()
}), })),
), ),
// Scaled sliced sprite horizontally // Scaled sliced sprite horizontally
( (
"Sliced and Tiled", "Sliced and Tiled",
style.clone(), style.clone(),
Vec2::new(300.0, 200.0), Vec2::new(300.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer { Some(ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border), border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.3 }, sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.3 },
..default() ..default()
}), })),
), ),
// Scaled sliced sprite horizontally with max scale // Scaled sliced sprite horizontally with max scale
( (
"Sliced and Tiled with corner constraint", "Sliced and Tiled with corner constraint",
style, style,
Vec2::new(300.0, 200.0), Vec2::new(300.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer { Some(ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border), border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.1 }, center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.1 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
max_corner_scale: 0.2, max_corner_scale: 0.2,
}), })),
), ),
]; ];
for (label, text_style, size, scale_mode) in cases { for (label, text_style, size, scale_mode) in cases {
position.x += 0.5 * size.x; position.x += 0.5 * size.x;
commands let mut cmd = commands.spawn(SpriteBundle {
.spawn(SpriteBundle { transform: Transform::from_translation(position),
transform: Transform::from_translation(position), texture: texture_handle.clone(),
texture: texture_handle.clone(), sprite: Sprite {
sprite: Sprite { custom_size: Some(size),
custom_size: Some(size), ..default()
..default() },
}, ..default()
scale_mode, });
if let Some(scale_mode) = scale_mode {
cmd.insert(scale_mode);
}
cmd.with_children(|builder| {
builder.spawn(Text2dBundle {
text: Text::from_section(label, text_style).with_justify(JustifyText::Center),
transform: Transform::from_xyz(0., -0.5 * size.y - 10., 0.0),
text_anchor: bevy::sprite::Anchor::TopCenter,
..default() ..default()
})
.with_children(|builder| {
builder.spawn(Text2dBundle {
text: Text::from_section(label, text_style).with_justify(JustifyText::Center),
transform: Transform::from_xyz(0., -0.5 * size.y - 10., 0.0),
text_anchor: bevy::sprite::Anchor::TopCenter,
..default()
});
}); });
});
position.x += 0.5 * size.x + gap; position.x += 0.5 * size.x + gap;
} }
} }

View File

@ -26,15 +26,17 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
current: 128.0, current: 128.0,
speed: 50.0, speed: 50.0,
}); });
commands.spawn(SpriteBundle { commands.spawn((
texture: asset_server.load("branding/icon.png"), SpriteBundle {
scale_mode: ImageScaleMode::Tiled { texture: asset_server.load("branding/icon.png"),
..default()
},
ImageScaleMode::Tiled {
tile_x: true, tile_x: true,
tile_y: true, tile_y: true,
stretch_value: 0.5, // The image will tile every 128px stretch_value: 0.5, // The image will tile every 128px
}, },
..default() ));
});
} }
fn animate(mut sprites: Query<&mut Sprite>, mut state: ResMut<AnimationState>, time: Res<Time>) { fn animate(mut sprites: Query<&mut Sprite>, mut state: ResMut<AnimationState>, time: Res<Time>) {

View File

@ -58,21 +58,23 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
.with_children(|parent| { .with_children(|parent| {
for [w, h] in [[150.0, 150.0], [300.0, 150.0], [150.0, 300.0]] { for [w, h] in [[150.0, 150.0], [300.0, 150.0], [150.0, 300.0]] {
parent parent
.spawn(ButtonBundle { .spawn((
style: Style { ButtonBundle {
width: Val::Px(w), style: Style {
height: Val::Px(h), width: Val::Px(w),
// horizontally center child text height: Val::Px(h),
justify_content: JustifyContent::Center, // horizontally center child text
// vertically center child text justify_content: JustifyContent::Center,
align_items: AlignItems::Center, // vertically center child text
margin: UiRect::all(Val::Px(20.0)), align_items: AlignItems::Center,
margin: UiRect::all(Val::Px(20.0)),
..default()
},
image: image.clone().into(),
..default() ..default()
}, },
image: image.clone().into(), ImageScaleMode::Sliced(slicer.clone()),
scale_mode: ImageScaleMode::Sliced(slicer.clone()), ))
..default()
})
.with_children(|parent| { .with_children(|parent| {
parent.spawn(TextBundle::from_section( parent.spawn(TextBundle::from_section(
"Button", "Button",