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_picking")]
14mod picking_backend;
15mod sprite;
16#[cfg(feature = "bevy_text")]
17mod text2d;
18mod texture_slice;
19
20pub mod prelude {
24 #[cfg(feature = "bevy_picking")]
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 SpriteScalingMode,
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_picking")]
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_asset::prelude::AssetChanged;
56use bevy_camera::visibility::NoAutoAabb;
57use bevy_ecs::prelude::*;
58use bevy_image::{Image, TextureAtlasLayout, TextureAtlasPlugin};
59use bevy_math::Vec2;
60
61#[derive(Default)]
63pub struct SpritePlugin;
64
65#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
67pub enum SpriteSystems {
68 ExtractSprites,
69 ComputeSlices,
70}
71
72impl Plugin for SpritePlugin {
73 fn build(&self, app: &mut App) {
74 if !app.is_plugin_added::<TextureAtlasPlugin>() {
75 app.add_plugins(TextureAtlasPlugin);
76 }
77 app.add_systems(
78 PostUpdate,
79 calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
80 );
81
82 #[cfg(feature = "bevy_text")]
83 app.add_systems(
84 PostUpdate,
85 (
86 bevy_text::detect_text_needs_rerender::<Text2d>,
87 update_text2d_layout
88 .after(bevy_camera::CameraUpdateSystems)
89 .after(bevy_text::free_unused_font_atlases_system),
90 calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds),
91 )
92 .chain()
93 .in_set(bevy_text::Text2dUpdateSystems)
94 .after(bevy_app::AnimationSystems),
95 );
96
97 #[cfg(feature = "bevy_picking")]
98 app.add_plugins(SpritePickingPlugin);
99 }
100}
101
102pub fn calculate_bounds_2d(
109 mut commands: Commands,
110 meshes: Res<Assets<Mesh>>,
111 images: Res<Assets<Image>>,
112 atlases: Res<Assets<TextureAtlasLayout>>,
113 new_mesh_aabb: Query<
114 (Entity, &Mesh2d),
115 (
116 Without<Aabb>,
117 Without<NoFrustumCulling>,
118 Without<NoAutoAabb>,
119 ),
120 >,
121 mut update_mesh_aabb: Query<
122 (&Mesh2d, &mut Aabb),
123 (
124 Or<(AssetChanged<Mesh2d>, Changed<Mesh2d>)>,
125 Without<NoFrustumCulling>,
126 Without<NoAutoAabb>,
127 Without<Sprite>, ),
129 >,
130 new_sprite_aabb: Query<
131 (Entity, &Sprite, &Anchor),
132 (
133 Without<Aabb>,
134 Without<NoFrustumCulling>,
135 Without<NoAutoAabb>,
136 ),
137 >,
138 mut update_sprite_aabb: Query<
139 (&Sprite, &mut Aabb, &Anchor),
140 (
141 Or<(Changed<Sprite>, Changed<Anchor>)>,
142 Without<NoFrustumCulling>,
143 Without<NoAutoAabb>,
144 Without<Mesh2d>, ),
146 >,
147) {
148 for (entity, mesh_handle) in &new_mesh_aabb {
150 if let Some(mesh) = meshes.get(mesh_handle)
151 && let Some(aabb) = mesh.compute_aabb()
152 {
153 commands.entity(entity).try_insert(aabb);
154 }
155 }
156
157 update_mesh_aabb
159 .par_iter_mut()
160 .for_each(|(mesh_handle, mut aabb)| {
161 if let Some(new_aabb) = meshes.get(mesh_handle).and_then(MeshAabb::compute_aabb) {
162 aabb.set_if_neq(new_aabb);
163 }
164 });
165
166 let sprite_size = |sprite: &Sprite| -> Option<Vec2> {
168 sprite
169 .custom_size
170 .or_else(|| sprite.rect.map(|rect| rect.size()))
171 .or_else(|| match &sprite.texture_atlas {
172 None => images.get(&sprite.image).map(Image::size_f32),
174 Some(atlas) => atlas
176 .texture_rect(&atlases)
177 .map(|rect| rect.size().as_vec2()),
178 })
179 };
180
181 for (size, (entity, anchor)) in new_sprite_aabb
183 .iter()
184 .filter_map(|(entity, sprite, anchor)| sprite_size(sprite).zip(Some((entity, anchor))))
185 {
186 let aabb = Aabb {
187 center: (-anchor.as_vec() * size).extend(0.0).into(),
188 half_extents: (0.5 * size).extend(0.0).into(),
189 };
190 commands.entity(entity).try_insert(aabb);
191 }
192
193 update_sprite_aabb
195 .par_iter_mut()
196 .for_each(|(sprite, mut aabb, anchor)| {
197 if let Some(size) = sprite_size(sprite) {
198 aabb.set_if_neq(Aabb {
199 center: (-anchor.as_vec() * size).extend(0.0).into(),
200 half_extents: (0.5 * size).extend(0.0).into(),
201 });
202 }
203 });
204}
205
206#[cfg(test)]
207mod test {
208 use super::*;
209 use bevy_math::{Rect, Vec2, Vec3A};
210
211 #[test]
212 fn calculate_bounds_2d_create_aabb_for_image_sprite_entity() {
213 let mut app = App::new();
215
216 let mut image_assets = Assets::<Image>::default();
218 let image_handle = image_assets.add(Image::default());
219 app.insert_resource(image_assets);
220 let mesh_assets = Assets::<Mesh>::default();
221 app.insert_resource(mesh_assets);
222 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
223 app.insert_resource(texture_atlas_assets);
224
225 app.add_systems(Update, calculate_bounds_2d);
227
228 let entity = app.world_mut().spawn(Sprite::from_image(image_handle)).id();
230
231 assert!(!app
233 .world()
234 .get_entity(entity)
235 .expect("Could not find entity")
236 .contains::<Aabb>());
237
238 app.update();
240
241 assert!(app
243 .world()
244 .get_entity(entity)
245 .expect("Could not find entity")
246 .contains::<Aabb>());
247 }
248
249 #[test]
250 fn calculate_bounds_2d_update_aabb_when_sprite_custom_size_changes_to_some() {
251 let mut app = App::new();
253
254 let mut image_assets = Assets::<Image>::default();
256 let image_handle = image_assets.add(Image::default());
257 app.insert_resource(image_assets);
258 let mesh_assets = Assets::<Mesh>::default();
259 app.insert_resource(mesh_assets);
260 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
261 app.insert_resource(texture_atlas_assets);
262
263 app.add_systems(Update, calculate_bounds_2d);
265
266 let entity = app
268 .world_mut()
269 .spawn(Sprite {
270 custom_size: Some(Vec2::ZERO),
271 image: image_handle,
272 ..Sprite::default()
273 })
274 .id();
275
276 app.update();
278
279 let first_aabb = *app
281 .world()
282 .get_entity(entity)
283 .expect("Could not find entity")
284 .get::<Aabb>()
285 .expect("Could not find initial AABB");
286
287 let mut binding = app
289 .world_mut()
290 .get_entity_mut(entity)
291 .expect("Could not find entity");
292 let mut sprite = binding
293 .get_mut::<Sprite>()
294 .expect("Could not find sprite component of entity");
295 sprite.custom_size = Some(Vec2::ONE);
296
297 app.update();
299
300 let second_aabb = *app
302 .world()
303 .get_entity(entity)
304 .expect("Could not find entity")
305 .get::<Aabb>()
306 .expect("Could not find second AABB");
307
308 assert_ne!(first_aabb, second_aabb);
310 }
311
312 #[test]
313 fn calculate_bounds_2d_correct_aabb_for_sprite_with_custom_rect() {
314 let mut app = App::new();
316
317 let mut image_assets = Assets::<Image>::default();
319 let image_handle = image_assets.add(Image::default());
320 app.insert_resource(image_assets);
321 let mesh_assets = Assets::<Mesh>::default();
322 app.insert_resource(mesh_assets);
323 let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
324 app.insert_resource(texture_atlas_assets);
325
326 app.add_systems(Update, calculate_bounds_2d);
328
329 let entity = app
331 .world_mut()
332 .spawn((
333 Sprite {
334 rect: Some(Rect::new(0., 0., 0.5, 1.)),
335 image: image_handle,
336 ..Sprite::default()
337 },
338 Anchor::TOP_RIGHT,
339 ))
340 .id();
341
342 app.update();
344
345 let aabb = *app
347 .world_mut()
348 .get_entity(entity)
349 .expect("Could not find entity")
350 .get::<Aabb>()
351 .expect("Could not find AABB");
352
353 assert_eq!(aabb.center, Vec3A::new(-0.25, -0.5, 0.));
355
356 assert_eq!(aabb.half_extents, Vec3A::new(0.25, 0.5, 0.));
358 }
359}