bevy_sprite/
lib.rs

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
9//! Provides 2D sprite functionality.
10
11extern 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
20/// The sprite prelude.
21///
22/// This includes the most common types in this crate, re-exported for your convenience.
23pub 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/// Adds support for 2D sprites.
59#[derive(Default)]
60pub struct SpritePlugin;
61
62/// System set for sprite rendering.
63#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
64pub enum SpriteSystems {
65    ExtractSprites,
66    ComputeSlices,
67}
68
69/// Deprecated alias for [`SpriteSystems`].
70#[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
103/// System calculating and inserting an [`Aabb`] component to entities with either:
104/// - a `Mesh2d` component,
105/// - a `Sprite` and `Handle<Image>` components,
106///   and without a [`NoFrustumCulling`] component.
107///
108/// Used in system set [`VisibilitySystems::CalculateBounds`].
109pub 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                // We default to the texture size for regular sprites
136                None => images.get(&sprite.image).map(Image::size_f32),
137                // We default to the drawn rect for atlas sprites
138                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        // Setup app
160        let mut app = App::new();
161
162        // Add resources and get handle to image
163        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        // Add system
172        app.add_systems(Update, calculate_bounds_2d);
173
174        // Add entities
175        let entity = app.world_mut().spawn(Sprite::from_image(image_handle)).id();
176
177        // Verify that the entity does not have an AABB
178        assert!(!app
179            .world()
180            .get_entity(entity)
181            .expect("Could not find entity")
182            .contains::<Aabb>());
183
184        // Run system
185        app.update();
186
187        // Verify the AABB exists
188        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        // Setup app
198        let mut app = App::new();
199
200        // Add resources and get handle to image
201        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        // Add system
210        app.add_systems(Update, calculate_bounds_2d);
211
212        // Add entities
213        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        // Create initial AABB
223        app.update();
224
225        // Get the initial AABB
226        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        // Change `custom_size` of sprite
234        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        // Re-run the `calculate_bounds_2d` system to get the new AABB
244        app.update();
245
246        // Get the re-calculated AABB
247        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        // Check that the AABBs are not equal
255        assert_ne!(first_aabb, second_aabb);
256    }
257
258    #[test]
259    fn calculate_bounds_2d_correct_aabb_for_sprite_with_custom_rect() {
260        // Setup app
261        let mut app = App::new();
262
263        // Add resources and get handle to image
264        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        // Add system
273        app.add_systems(Update, calculate_bounds_2d);
274
275        // Add entities
276        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        // Create AABB
289        app.update();
290
291        // Get the AABB
292        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        // Verify that the AABB is at the expected position
300        assert_eq!(aabb.center, Vec3A::new(-0.25, -0.5, 0.));
301
302        // Verify that the AABB has the expected size
303        assert_eq!(aabb.half_extents, Vec3A::new(0.25, 0.5, 0.));
304    }
305}