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_ecs::bundle::Bundle;
use bevy_render::{
@ -8,12 +8,16 @@ use bevy_render::{
use bevy_transform::components::{GlobalTransform, Transform};
/// 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)]
pub struct SpriteBundle {
/// Specifies the rendering properties of the sprite, such as color tint and flip.
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.
pub transform: Transform,
/// The absolute transform of the sprite. This should generally not be written to directly.
@ -41,8 +45,6 @@ pub struct SpriteBundle {
pub struct SpriteSheetBundle {
/// Specifies the rendering properties of the sprite, such as color tint and flip.
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.
pub transform: Transform,
/// 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.
#[derive(Component, Debug, Default, Clone, Reflect)]
#[reflect(Component, Default)]
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component)]
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
Sliced(TextureSlicer),
/// 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_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
#[derive(Debug, Clone, Component)]
@ -62,9 +62,7 @@ impl ComputedTextureSlices {
/// 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.
///
/// Returns `None` if either:
/// - The scale mode is [`ImageScaleMode::Stretched`]
/// - The image asset is not loaded
/// Returns `None` if the image asset is not loaded
#[must_use]
fn compute_sprite_slices(
sprite: &Sprite,
@ -72,9 +70,6 @@ fn compute_sprite_slices(
image_handle: &Handle<Image>,
images: &Assets<Image>,
) -> Option<ComputedTextureSlices> {
if let ImageScaleMode::Stretched = scale_mode {
return None;
}
let image_size = images.get(image_handle).map(|i| {
Vec2::new(
i.texture_descriptor.size.width as f32,
@ -82,7 +77,6 @@ fn compute_sprite_slices(
)
})?;
let slices = match scale_mode {
ImageScaleMode::Stretched => unreachable!(),
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(
sprite.rect.unwrap_or(Rect {
min: Vec2::ZERO,
@ -110,7 +104,7 @@ fn compute_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(
mut commands: Commands,
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
/// on matching sprite entities with a [`ImageScaleMode`] component
pub(crate) fn compute_slices_on_sprite_change(
mut commands: Commands,
images: Res<Assets<Image>>,

View file

@ -13,7 +13,7 @@ use bevy_render::{
prelude::Color,
view::{InheritedVisibility, ViewVisibility, Visibility},
};
use bevy_sprite::{ImageScaleMode, TextureAtlas};
use bevy_sprite::TextureAtlas;
#[cfg(feature = "bevy_text")]
use bevy_text::{BreakLineOn, JustifyText, Text, TextLayoutInfo, TextSection, TextStyle};
use bevy_transform::prelude::{GlobalTransform, Transform};
@ -76,6 +76,11 @@ impl Default for NodeBundle {
}
/// 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)]
pub struct ImageBundle {
/// Describes the logical size of the node
@ -95,8 +100,6 @@ pub struct ImageBundle {
///
/// This component is set automatically
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
pub focus_policy: FocusPolicy,
/// The transform of the node
@ -288,6 +291,11 @@ where
}
/// 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)]
pub struct ButtonBundle {
/// Describes the logical size of the node
@ -309,8 +317,6 @@ pub struct ButtonBundle {
pub border_color: BorderColor,
/// The image of the node
pub image: UiImage,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// The transform of the node
///
/// This component is automatically managed by the UI layout system.
@ -347,7 +353,6 @@ impl Default for ButtonBundle {
inherited_visibility: Default::default(),
view_visibility: 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
/// will be computed according to the `image_handle` dimensions or the sprite rect.
///
/// Returns `None` if either:
/// - The scale mode is [`ImageScaleMode::Stretched`]
/// - The image asset is not loaded
/// Returns `None` if the image asset is not loaded
#[must_use]
fn compute_texture_slices(
draw_area: Vec2,
@ -87,9 +85,6 @@ fn compute_texture_slices(
image_handle: &UiImage,
images: &Assets<Image>,
) -> Option<ComputedTextureSlices> {
if let ImageScaleMode::Stretched = scale_mode {
return None;
}
let image_size = images.get(&image_handle.texture).map(|i| {
Vec2::new(
i.texture_descriptor.size.width as f32,
@ -101,7 +96,6 @@ fn compute_texture_slices(
max: image_size,
};
let slices = match scale_mode {
ImageScaleMode::Stretched => unreachable!(),
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, Some(draw_area)),
ImageScaleMode::Tiled {
tile_x,
@ -120,7 +114,7 @@ fn compute_texture_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(
mut commands: Commands,
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
/// on matching sprite entities with a [`ImageScaleMode`] component
pub(crate) fn compute_slices_on_image_change(
mut commands: Commands,
images: Res<Assets<Image>>,

View file

@ -25,89 +25,85 @@ fn spawn_sprites(
) {
let cases = [
// Reference sprite
(
"Original texture",
style.clone(),
Vec2::splat(100.0),
ImageScaleMode::default(),
),
("Original texture", style.clone(), Vec2::splat(100.0), None),
// Scaled regular sprite
(
"Stretched texture",
style.clone(),
Vec2::new(100.0, 200.0),
ImageScaleMode::default(),
None,
),
// Stretched Scaled sliced sprite
(
"Stretched and sliced",
style.clone(),
Vec2::new(100.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer {
Some(ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Stretch,
..default()
}),
})),
),
// Scaled sliced sprite
(
"Sliced and Tiled",
style.clone(),
Vec2::new(100.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer {
Some(ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.5 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
..default()
}),
})),
),
// Scaled sliced sprite horizontally
(
"Sliced and Tiled",
style.clone(),
Vec2::new(300.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer {
Some(ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.3 },
..default()
}),
})),
),
// Scaled sliced sprite horizontally with max scale
(
"Sliced and Tiled with corner constraint",
style,
Vec2::new(300.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer {
Some(ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.1 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
max_corner_scale: 0.2,
}),
})),
),
];
for (label, text_style, size, scale_mode) in cases {
position.x += 0.5 * size.x;
commands
.spawn(SpriteBundle {
transform: Transform::from_translation(position),
texture: texture_handle.clone(),
sprite: Sprite {
custom_size: Some(size),
..default()
},
scale_mode,
let mut cmd = commands.spawn(SpriteBundle {
transform: Transform::from_translation(position),
texture: texture_handle.clone(),
sprite: Sprite {
custom_size: Some(size),
..default()
},
..default()
});
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()
})
.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;
}
}

View file

@ -26,15 +26,17 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
current: 128.0,
speed: 50.0,
});
commands.spawn(SpriteBundle {
texture: asset_server.load("branding/icon.png"),
scale_mode: ImageScaleMode::Tiled {
commands.spawn((
SpriteBundle {
texture: asset_server.load("branding/icon.png"),
..default()
},
ImageScaleMode::Tiled {
tile_x: true,
tile_y: true,
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>) {

View file

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