Fix Anchor component inconsistancies (#18393)

## Objective

Fix the misleading 2d anchor API where `Anchor` is a component and
required by `Text2d` but is stored on a field for sprites.

Fixes #18367

## Solution

Remove the `anchor` field from `Sprite` and require `Anchor` instead.

## Migration Guide

The `anchor` field has been removed from `Sprite`. Instead the `Anchor`
component is now a required component on `Sprite`.
This commit is contained in:
ickshonpe 2025-05-21 16:32:04 +01:00 committed by GitHub
parent bf20c630a8
commit 2b59ab8e2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 89 additions and 55 deletions

View File

@ -169,9 +169,9 @@ pub fn calculate_bounds_2d(
atlases: Res<Assets<TextureAtlasLayout>>,
meshes_without_aabb: Query<(Entity, &Mesh2d), (Without<Aabb>, Without<NoFrustumCulling>)>,
sprites_to_recalculate_aabb: Query<
(Entity, &Sprite),
(Entity, &Sprite, &Anchor),
(
Or<(Without<Aabb>, Changed<Sprite>)>,
Or<(Without<Aabb>, Changed<Sprite>, Changed<Anchor>)>,
Without<NoFrustumCulling>,
),
>,
@ -183,7 +183,7 @@ pub fn calculate_bounds_2d(
}
}
}
for (entity, sprite) in &sprites_to_recalculate_aabb {
for (entity, sprite, anchor) in &sprites_to_recalculate_aabb {
if let Some(size) = sprite
.custom_size
.or_else(|| sprite.rect.map(|rect| rect.size()))
@ -197,7 +197,7 @@ pub fn calculate_bounds_2d(
})
{
let aabb = Aabb {
center: (-sprite.anchor.as_vec() * size).extend(0.0).into(),
center: (-anchor.as_vec() * size).extend(0.0).into(),
half_extents: (0.5 * size).extend(0.0).into(),
};
commands.entity(entity).try_insert(aabb);
@ -334,12 +334,14 @@ mod test {
// Add entities
let entity = app
.world_mut()
.spawn(Sprite {
rect: Some(Rect::new(0., 0., 0.5, 1.)),
anchor: Anchor::TOP_RIGHT,
image: image_handle,
..default()
})
.spawn((
Sprite {
rect: Some(Rect::new(0., 0., 0.5, 1.)),
image: image_handle,
..default()
},
Anchor::TOP_RIGHT,
))
.id();
// Create AABB

View File

@ -10,7 +10,7 @@
//! - The `position` reported in `HitData` in in world space, and the `normal` is a normalized
//! vector provided by the target's `GlobalTransform::back()`.
use crate::Sprite;
use crate::{Anchor, Sprite};
use bevy_app::prelude::*;
use bevy_asset::prelude::*;
use bevy_color::Alpha;
@ -100,6 +100,7 @@ fn sprite_picking(
Entity,
&Sprite,
&GlobalTransform,
&Anchor,
&Pickable,
&ViewVisibility,
)>,
@ -107,9 +108,9 @@ fn sprite_picking(
) {
let mut sorted_sprites: Vec<_> = sprite_query
.iter()
.filter_map(|(entity, sprite, transform, pickable, vis)| {
.filter_map(|(entity, sprite, transform, anchor, pickable, vis)| {
if !transform.affine().is_nan() && vis.get() {
Some((entity, sprite, transform, pickable))
Some((entity, sprite, transform, anchor, pickable))
} else {
None
}
@ -117,7 +118,7 @@ fn sprite_picking(
.collect();
// radsort is a stable radix sort that performed better than `slice::sort_by_key`
radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _)| {
radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _, _)| {
-transform.translation().z
});
@ -159,7 +160,7 @@ fn sprite_picking(
let picks: Vec<(Entity, HitData)> = sorted_sprites
.iter()
.copied()
.filter_map(|(entity, sprite, sprite_transform, pickable)| {
.filter_map(|(entity, sprite, sprite_transform, anchor, pickable)| {
if blocked {
return None;
}
@ -192,6 +193,7 @@ fn sprite_picking(
let Ok(cursor_pixel_space) = sprite.compute_pixel_space_point(
cursor_pos_sprite,
*anchor,
&images,
&texture_atlas_layout,
) else {

View File

@ -1,6 +1,6 @@
use core::ops::Range;
use crate::{ComputedTextureSlices, ScalingMode, Sprite, SPRITE_SHADER_HANDLE};
use crate::{Anchor, ComputedTextureSlices, ScalingMode, Sprite, SPRITE_SHADER_HANDLE};
use bevy_asset::{AssetEvent, AssetId, Assets};
use bevy_color::{ColorToComponents, LinearRgba};
use bevy_core_pipeline::{
@ -394,13 +394,14 @@ pub fn extract_sprites(
&ViewVisibility,
&Sprite,
&GlobalTransform,
&Anchor,
Option<&ComputedTextureSlices>,
)>,
>,
) {
extracted_sprites.sprites.clear();
extracted_slices.slices.clear();
for (main_entity, render_entity, view_visibility, sprite, transform, slices) in
for (main_entity, render_entity, view_visibility, sprite, transform, anchor, slices) in
sprite_query.iter()
{
if !view_visibility.get() {
@ -411,7 +412,7 @@ pub fn extract_sprites(
let start = extracted_slices.slices.len();
extracted_slices
.slices
.extend(slices.extract_slices(sprite));
.extend(slices.extract_slices(sprite, anchor.as_vec()));
let end = extracted_slices.slices.len();
extracted_sprites.sprites.push(ExtractedSprite {
main_entity,
@ -451,7 +452,7 @@ pub fn extract_sprites(
flip_y: sprite.flip_y,
image_handle_id: sprite.image.id(),
kind: ExtractedSpriteKind::Single {
anchor: sprite.anchor.as_vec(),
anchor: anchor.as_vec(),
rect,
scaling_mode: sprite.image_mode.scale(),
// Pass the custom size

View File

@ -15,7 +15,7 @@ use crate::TextureSlicer;
/// Describes a sprite to be rendered to a 2D camera
#[derive(Component, Debug, Default, Clone, Reflect)]
#[require(Transform, Visibility, SyncToRenderWorld, VisibilityClass)]
#[require(Transform, Visibility, SyncToRenderWorld, VisibilityClass, Anchor)]
#[reflect(Component, Default, Debug, Clone)]
#[component(on_add = view::add_visibility_class::<Sprite>)]
pub struct Sprite {
@ -38,8 +38,6 @@ pub struct Sprite {
/// When used with a [`TextureAtlas`], the rect
/// is offset by the atlas's minimal (top-left) corner position.
pub rect: Option<Rect>,
/// [`Anchor`] point of the sprite in the world
pub anchor: Anchor,
/// How the sprite's image will be scaled.
pub image_mode: SpriteImageMode,
}
@ -86,6 +84,7 @@ impl Sprite {
pub fn compute_pixel_space_point(
&self,
point_relative_to_sprite: Vec2,
anchor: Anchor,
images: &Assets<Image>,
texture_atlases: &Assets<TextureAtlasLayout>,
) -> Result<Vec2, Vec2> {
@ -112,7 +111,7 @@ impl Sprite {
};
let sprite_size = self.custom_size.unwrap_or_else(|| texture_rect.size());
let sprite_center = -self.anchor.as_vec() * sprite_size;
let sprite_center = -anchor.as_vec() * sprite_size;
let mut point_relative_to_sprite_center = point_relative_to_sprite - sprite_center;
@ -315,8 +314,14 @@ mod tests {
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
let compute = |point| {
sprite.compute_pixel_space_point(
point,
Anchor::default(),
&image_assets,
&texture_atlas_assets,
)
};
assert_eq!(compute(Vec2::new(-2.0, -4.5)), Ok(Vec2::new(0.5, 9.5)));
assert_eq!(compute(Vec2::new(0.0, 0.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(0.0, 4.5)), Ok(Vec2::new(2.5, 0.5)));
@ -334,7 +339,12 @@ mod tests {
let compute = |point| {
sprite
.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets)
.compute_pixel_space_point(
point,
Anchor::default(),
&image_assets,
&texture_atlas_assets,
)
// Round to remove floating point errors.
.map(|x| (x * 1e5).round() / 1e5)
.map_err(|x| (x * 1e5).round() / 1e5)
@ -355,12 +365,13 @@ mod tests {
let sprite = Sprite {
image,
anchor: Anchor::BOTTOM_LEFT,
..Default::default()
};
let anchor = Anchor::BOTTOM_LEFT;
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
let compute = |point| {
sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets)
};
assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(0.5, 0.5)));
assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5)));
@ -377,12 +388,13 @@ mod tests {
let sprite = Sprite {
image,
anchor: Anchor::TOP_RIGHT,
..Default::default()
};
let anchor = Anchor::TOP_RIGHT;
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
let compute = |point| {
sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets)
};
assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 0.5)));
assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 0.5)));
@ -399,13 +411,14 @@ mod tests {
let sprite = Sprite {
image,
anchor: Anchor::BOTTOM_LEFT,
flip_x: true,
..Default::default()
};
let anchor = Anchor::BOTTOM_LEFT;
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
let compute = |point| {
sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets)
};
assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(4.5, 0.5)));
assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5)));
@ -422,13 +435,14 @@ mod tests {
let sprite = Sprite {
image,
anchor: Anchor::TOP_RIGHT,
flip_y: true,
..Default::default()
};
let anchor = Anchor::TOP_RIGHT;
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
let compute = |point| {
sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets)
};
assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 9.5)));
assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0)));
assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 9.5)));
@ -446,12 +460,13 @@ mod tests {
let sprite = Sprite {
image,
rect: Some(Rect::new(1.5, 3.0, 3.0, 9.5)),
anchor: Anchor::BOTTOM_LEFT,
..Default::default()
};
let anchor = Anchor::BOTTOM_LEFT;
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
let compute = |point| {
sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets)
};
assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(2.0, 9.0)));
// The pixel is outside the rect, but is still a valid pixel in the image.
assert_eq!(compute(Vec2::new(2.0, 2.5)), Err(Vec2::new(3.5, 7.0)));
@ -470,16 +485,17 @@ mod tests {
let sprite = Sprite {
image,
anchor: Anchor::BOTTOM_LEFT,
texture_atlas: Some(TextureAtlas {
layout: texture_atlas,
index: 0,
}),
..Default::default()
};
let anchor = Anchor::BOTTOM_LEFT;
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
let compute = |point| {
sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets)
};
assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(1.5, 3.5)));
// The pixel is outside the texture atlas, but is still a valid pixel in the image.
assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(5.0, 1.5)));
@ -498,7 +514,6 @@ mod tests {
let sprite = Sprite {
image,
anchor: Anchor::BOTTOM_LEFT,
texture_atlas: Some(TextureAtlas {
layout: texture_atlas,
index: 0,
@ -507,9 +522,11 @@ mod tests {
rect: Some(Rect::new(1.5, 1.5, 3.0, 3.0)),
..Default::default()
};
let anchor = Anchor::BOTTOM_LEFT;
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
let compute = |point| {
sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets)
};
assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(3.0, 3.5)));
// The pixel is outside the texture atlas, but is still a valid pixel in the image.
assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(6.5, 1.5)));
@ -529,8 +546,14 @@ mod tests {
..Default::default()
};
let compute =
|point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets);
let compute = |point| {
sprite.compute_pixel_space_point(
point,
Anchor::default(),
&image_assets,
&texture_atlas_assets,
)
};
assert_eq!(compute(Vec2::new(30.0, 15.0)), Ok(Vec2::new(4.0, 1.0)));
assert_eq!(compute(Vec2::new(-10.0, -15.0)), Ok(Vec2::new(2.0, 4.0)));
// The pixel is outside the texture atlas, but is still a valid pixel in the image.

View File

@ -1,6 +1,5 @@
use crate::{ExtractedSlice, Sprite, SpriteImageMode, TextureAtlasLayout};
use super::TextureSlice;
use crate::{ExtractedSlice, Sprite, SpriteImageMode, TextureAtlasLayout};
use bevy_asset::{AssetEvent, Assets};
use bevy_ecs::prelude::*;
use bevy_image::Image;
@ -23,6 +22,7 @@ impl ComputedTextureSlices {
pub(crate) fn extract_slices<'a>(
&'a self,
sprite: &'a Sprite,
anchor: Vec2,
) -> impl ExactSizeIterator<Item = ExtractedSlice> + 'a {
let mut flip = Vec2::ONE;
if sprite.flip_x {
@ -31,7 +31,7 @@ impl ComputedTextureSlices {
if sprite.flip_y {
flip.y *= -1.0;
}
let anchor = sprite.anchor.as_vec()
let anchor = anchor
* sprite
.custom_size
.unwrap_or(sprite.rect.unwrap_or_default().size());

View File

@ -72,9 +72,9 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
image: asset_server.load("branding/bevy_bird_dark.png"),
custom_size: Some(sprite_size),
color: Color::srgb(1.0, 0.0, 0.0),
anchor: anchor.to_owned(),
..default()
},
anchor.to_owned(),
// 3x3 grid of anchor examples by changing transform
Transform::from_xyz(i * len - len, j * len - len, 0.0)
.with_scale(Vec3::splat(1.0 + (i - 1.0) * 0.2))

View File

@ -246,10 +246,10 @@ mod text {
Sprite {
color: palettes::tailwind::GRAY_900.into(),
custom_size: Some(Vec2::new(bounds.width.unwrap(), bounds.height.unwrap())),
anchor,
..Default::default()
},
Transform::from_translation(dest - Vec3::Z),
anchor,
DespawnOnExitState(super::Scene::Text),
));
}
@ -273,12 +273,12 @@ mod sprite {
commands.spawn((
Sprite {
image: asset_server.load("branding/bevy_logo_dark.png"),
anchor,
flip_x,
flip_y,
color,
..default()
},
anchor,
DespawnOnExitState(super::Scene::Sprite),
));
}

View File

@ -0,0 +1,6 @@
---
title: `Anchor` is now a required component on `Sprite`
pull_requests: [18393]
---
The `anchor` field has been removed from `Sprite`. Instead the `Anchor` component is now a required component on `Sprite`.