1#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![forbid(unsafe_code)]
4#![doc(
5 html_logo_url = "https://bevy.org/assets/icon.png",
6 html_favicon_url = "https://bevy.org/assets/icon.png"
7)]
8
9extern crate alloc;
12
13#[cfg(feature = "bevy_sprite_picking_backend")]
14mod picking_backend;
15mod sprite;
16#[cfg(feature = "bevy_text")]
17mod text2d;
18mod texture_slice;
19
20pub mod prelude {
24 #[cfg(feature = "bevy_sprite_picking_backend")]
25 #[doc(hidden)]
26 pub use crate::picking_backend::{
27 SpritePickingCamera, SpritePickingMode, SpritePickingPlugin, SpritePickingSettings,
28 };
29 #[cfg(feature = "bevy_text")]
30 #[doc(hidden)]
31 pub use crate::text2d::{Text2d, Text2dReader, Text2dWriter};
32 #[doc(hidden)]
33 pub use crate::{
34 sprite::{Sprite, SpriteImageMode},
35 texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
36 ScalingMode,
37 };
38}
39
40use bevy_asset::Assets;
41use bevy_camera::{
42 primitives::{Aabb, MeshAabb},
43 visibility::NoFrustumCulling,
44 visibility::VisibilitySystems,
45};
46use bevy_mesh::{Mesh, Mesh2d};
47#[cfg(feature = "bevy_sprite_picking_backend")]
48pub use picking_backend::*;
49pub use sprite::*;
50#[cfg(feature = "bevy_text")]
51pub use text2d::*;
52pub use texture_slice::*;
53
54use bevy_app::prelude::*;
55use bevy_ecs::prelude::*;
56use bevy_image::{Image, TextureAtlasLayout, TextureAtlasPlugin};
57
58#[derive(Default)]
60pub struct SpritePlugin;
61
62#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
64pub enum SpriteSystems {
65 ExtractSprites,
66 ComputeSlices,
67}
68
69#[deprecated(since = "0.17.0", note = "Renamed to `SpriteSystems`.")]
71pub type SpriteSystem = SpriteSystems;
72
73impl Plugin for SpritePlugin {
74 fn build(&self, app: &mut App) {
75 if !app.is_plugin_added::<TextureAtlasPlugin>() {
76 app.add_plugins(TextureAtlasPlugin);
77 }
78 app.add_systems(
79 PostUpdate,
80 calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
81 );
82
83 #[cfg(feature = "bevy_text")]
84 app.add_systems(
85 PostUpdate,
86 (
87 bevy_text::detect_text_needs_rerender::<Text2d>,
88 update_text2d_layout
89 .after(bevy_camera::CameraUpdateSystems)
90 .after(bevy_text::remove_dropped_font_atlas_sets),
91 calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds),
92 )
93 .chain()
94 .in_set(bevy_text::Text2dUpdateSystems)
95 .after(bevy_app::AnimationSystems),
96 );
97
98 #[cfg(feature = "bevy_sprite_picking_backend")]
99 app.add_plugins(SpritePickingPlugin);
100 }
101}
102
103pub fn calculate_bounds_2d(
110 mut commands: Commands,
111 meshes: Res<Assets<Mesh>>,
112 images: Res<Assets<Image>>,
113 atlases: Res<Assets<TextureAtlasLayout>>,
114 meshes_without_aabb: Query<(Entity, &Mesh2d), (Without<Aabb>, Without<NoFrustumCulling>)>,
115 sprites_to_recalculate_aabb: Query<
116 (Entity, &Sprite, &Anchor),
117 (
118 Or<(Without<Aabb>, Changed<Sprite>, Changed<Anchor>)>,
119 Without<NoFrustumCulling>,
120 ),
121 >,
122) {
123 for (entity, mesh_handle) in &meshes_without_aabb {
124 if let Some(mesh) = meshes.get(&mesh_handle.0)
125 && let Some(aabb) = mesh.compute_aabb()
126 {
127 commands.entity(entity).try_insert(aabb);
128 }
129 }
130 for (entity, sprite, anchor) in &sprites_to_recalculate_aabb {
131 if let Some(size) = sprite
132 .custom_size
133 .or_else(|| sprite.rect.map(|rect| rect.size()))
134 .or_else(|| match &sprite.texture_atlas {
135 None => images.get(&sprite.image).map(Image::size_f32),
137 Some(atlas) => atlas
139 .texture_rect(&atlases)
140 .map(|rect| rect.size().as_vec2()),
141 })
142 {
143 let aabb = Aabb {
144 center: (-anchor.as_vec() * size).extend(0.0).into(),
145 half_extents: (0.5 * size).extend(0.0).into(),
146 };
147 commands.entity(entity).try_insert(aabb);
148 }
149 }
150}
151
152#[cfg(test)]
153mod test {
154 use super::*;
155 use bevy_math::{Rect, Vec2, Vec3A};
156
157 #[test]
158 fn calculate_bounds_2d_create_aabb_for_image_sprite_entity() {
159 let mut app = App::new();
161
162 let mut image_assets = Assets::<Image>::default();
164 let image_handle = image_assets.add(Image::default());
165 app.insert_resource(image_assets);
166 let mesh_assets = Assets::<Mesh>::default();
167 app.insert_resource(mesh_assets);
168 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
169 app.insert_resource(texture_atlas_assets);
170
171 app.add_systems(Update, calculate_bounds_2d);
173
174 let entity = app.world_mut().spawn(Sprite::from_image(image_handle)).id();
176
177 assert!(!app
179 .world()
180 .get_entity(entity)
181 .expect("Could not find entity")
182 .contains::<Aabb>());
183
184 app.update();
186
187 assert!(app
189 .world()
190 .get_entity(entity)
191 .expect("Could not find entity")
192 .contains::<Aabb>());
193 }
194
195 #[test]
196 fn calculate_bounds_2d_update_aabb_when_sprite_custom_size_changes_to_some() {
197 let mut app = App::new();
199
200 let mut image_assets = Assets::<Image>::default();
202 let image_handle = image_assets.add(Image::default());
203 app.insert_resource(image_assets);
204 let mesh_assets = Assets::<Mesh>::default();
205 app.insert_resource(mesh_assets);
206 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
207 app.insert_resource(texture_atlas_assets);
208
209 app.add_systems(Update, calculate_bounds_2d);
211
212 let entity = app
214 .world_mut()
215 .spawn(Sprite {
216 custom_size: Some(Vec2::ZERO),
217 image: image_handle,
218 ..Sprite::default()
219 })
220 .id();
221
222 app.update();
224
225 let first_aabb = *app
227 .world()
228 .get_entity(entity)
229 .expect("Could not find entity")
230 .get::<Aabb>()
231 .expect("Could not find initial AABB");
232
233 let mut binding = app
235 .world_mut()
236 .get_entity_mut(entity)
237 .expect("Could not find entity");
238 let mut sprite = binding
239 .get_mut::<Sprite>()
240 .expect("Could not find sprite component of entity");
241 sprite.custom_size = Some(Vec2::ONE);
242
243 app.update();
245
246 let second_aabb = *app
248 .world()
249 .get_entity(entity)
250 .expect("Could not find entity")
251 .get::<Aabb>()
252 .expect("Could not find second AABB");
253
254 assert_ne!(first_aabb, second_aabb);
256 }
257
258 #[test]
259 fn calculate_bounds_2d_correct_aabb_for_sprite_with_custom_rect() {
260 let mut app = App::new();
262
263 let mut image_assets = Assets::<Image>::default();
265 let image_handle = image_assets.add(Image::default());
266 app.insert_resource(image_assets);
267 let mesh_assets = Assets::<Mesh>::default();
268 app.insert_resource(mesh_assets);
269 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
270 app.insert_resource(texture_atlas_assets);
271
272 app.add_systems(Update, calculate_bounds_2d);
274
275 let entity = app
277 .world_mut()
278 .spawn((
279 Sprite {
280 rect: Some(Rect::new(0., 0., 0.5, 1.)),
281 image: image_handle,
282 ..Sprite::default()
283 },
284 Anchor::TOP_RIGHT,
285 ))
286 .id();
287
288 app.update();
290
291 let aabb = *app
293 .world_mut()
294 .get_entity(entity)
295 .expect("Could not find entity")
296 .get::<Aabb>()
297 .expect("Could not find AABB");
298
299 assert_eq!(aabb.center, Vec3A::new(-0.25, -0.5, 0.));
301
302 assert_eq!(aabb.half_extents, Vec3A::new(0.25, 0.5, 0.));
304 }
305}