@@ -10,15 +10,17 @@ use bevy_ecs::{
1010    entity:: Entity , 
1111    event:: EventReader , 
1212    prelude:: With , 
13+     query:: { Changed ,  Without } , 
1314    reflect:: ReflectComponent , 
1415    system:: { Commands ,  Local ,  Query ,  Res ,  ResMut } , 
1516} ; 
1617use  bevy_math:: Vec2 ; 
1718use  bevy_reflect:: Reflect ; 
1819use  bevy_render:: { 
1920    prelude:: LegacyColor , 
21+     primitives:: Aabb , 
2022    texture:: Image , 
21-     view:: { InheritedVisibility ,  ViewVisibility ,  Visibility } , 
23+     view:: { InheritedVisibility ,  NoFrustumCulling ,   ViewVisibility ,  Visibility } , 
2224    Extract , 
2325} ; 
2426use  bevy_sprite:: { Anchor ,  ExtractedSprite ,  ExtractedSprites ,  TextureAtlasLayout } ; 
@@ -226,3 +228,152 @@ pub fn update_text2d_layout(
226228pub  fn  scale_value ( value :  f32 ,  factor :  f32 )  -> f32  { 
227229    value *  factor
228230} 
231+ 
232+ /// System calculating and inserting an [`Aabb`] component to entities with some 
233+ /// [`TextLayoutInfo`] and [`Anchor`] components, and without a [`NoFrustumCulling`] component. 
234+ /// 
235+ /// Used in system set [`VisibilitySystems::CalculateBounds`](bevy_render::view::VisibilitySystems::CalculateBounds). 
236+ pub  fn  calculate_bounds_text2d ( 
237+     mut  commands :  Commands , 
238+     mut  text_to_update_aabb :  Query < 
239+         ( Entity ,  & TextLayoutInfo ,  & Anchor ,  Option < & mut  Aabb > ) , 
240+         ( Changed < TextLayoutInfo > ,  Without < NoFrustumCulling > ) , 
241+     > , 
242+ )  { 
243+     for  ( entity,  layout_info,  anchor,  aabb)  in  & mut  text_to_update_aabb { 
244+         // `Anchor::as_vec` gives us an offset relative to the text2d bounds, by negating it and scaling 
245+         // by the logical size we compensate the transform offset in local space to get the center. 
246+         let  center = ( -anchor. as_vec ( )  *  layout_info. logical_size ) 
247+             . extend ( 0.0 ) 
248+             . into ( ) ; 
249+         // Distance in local space from the center to the x and y limits of the text2d bounds. 
250+         let  half_extents = ( layout_info. logical_size  / 2.0 ) . extend ( 0.0 ) . into ( ) ; 
251+         if  let  Some ( mut  aabb)  = aabb { 
252+             * aabb = Aabb  { 
253+                 center, 
254+                 half_extents, 
255+             } ; 
256+         }  else  { 
257+             commands. entity ( entity) . try_insert ( Aabb  { 
258+                 center, 
259+                 half_extents, 
260+             } ) ; 
261+         } 
262+     } 
263+ } 
264+ 
265+ #[ cfg( test) ]  
266+ mod  tests { 
267+ 
268+     use  bevy_app:: { App ,  Update } ; 
269+     use  bevy_asset:: { load_internal_binary_asset,  Handle } ; 
270+     use  bevy_ecs:: { event:: Events ,  schedule:: IntoSystemConfigs } ; 
271+     use  bevy_utils:: default; 
272+ 
273+     use  super :: * ; 
274+ 
275+     const  FIRST_TEXT :  & str  = "Sample text." ; 
276+     const  SECOND_TEXT :  & str  = "Another, longer sample text." ; 
277+ 
278+     fn  setup ( )  -> ( App ,  Entity )  { 
279+         let  mut  app = App :: new ( ) ; 
280+         app. init_resource :: < Assets < Font > > ( ) 
281+             . init_resource :: < Assets < Image > > ( ) 
282+             . init_resource :: < Assets < TextureAtlasLayout > > ( ) 
283+             . init_resource :: < TextSettings > ( ) 
284+             . init_resource :: < FontAtlasSets > ( ) 
285+             . init_resource :: < Events < WindowScaleFactorChanged > > ( ) 
286+             . insert_resource ( TextPipeline :: default ( ) ) 
287+             . add_systems ( 
288+                 Update , 
289+                 ( 
290+                     update_text2d_layout, 
291+                     calculate_bounds_text2d. after ( update_text2d_layout) , 
292+                 ) , 
293+             ) ; 
294+ 
295+         // A font is needed to ensure the text is laid out with an actual size. 
296+         load_internal_binary_asset ! ( 
297+             app, 
298+             Handle :: default ( ) , 
299+             "FiraMono-subset.ttf" , 
300+             |bytes:  & [ u8 ] ,  _path:  String | {  Font :: try_from_bytes( bytes. to_vec( ) ) . unwrap( )  } 
301+         ) ; 
302+ 
303+         let  entity = app
304+             . world 
305+             . spawn ( ( Text2dBundle  { 
306+                 text :  Text :: from_section ( FIRST_TEXT ,  default ( ) ) , 
307+                 ..default ( ) 
308+             } , ) ) 
309+             . id ( ) ; 
310+ 
311+         ( app,  entity) 
312+     } 
313+ 
314+     #[ test]  
315+     fn  calculate_bounds_text2d_create_aabb ( )  { 
316+         let  ( mut  app,  entity)  = setup ( ) ; 
317+ 
318+         assert ! ( !app
319+             . world
320+             . get_entity( entity) 
321+             . expect( "Could not find entity" ) 
322+             . contains:: <Aabb >( ) ) ; 
323+ 
324+         // Creates the AABB after text layouting. 
325+         app. update ( ) ; 
326+ 
327+         let  aabb = app
328+             . world 
329+             . get_entity ( entity) 
330+             . expect ( "Could not find entity" ) 
331+             . get :: < Aabb > ( ) 
332+             . expect ( "Text should have an AABB" ) ; 
333+ 
334+         // Text2D AABB does not have a depth. 
335+         assert_eq ! ( aabb. center. z,  0.0 ) ; 
336+         assert_eq ! ( aabb. half_extents. z,  0.0 ) ; 
337+ 
338+         // AABB has an actual size. 
339+         assert ! ( aabb. half_extents. x > 0.0  && aabb. half_extents. y > 0.0 ) ; 
340+     } 
341+ 
342+     #[ test]  
343+     fn  calculate_bounds_text2d_update_aabb ( )  { 
344+         let  ( mut  app,  entity)  = setup ( ) ; 
345+ 
346+         // Creates the initial AABB after text layouting. 
347+         app. update ( ) ; 
348+ 
349+         let  first_aabb = * app
350+             . world 
351+             . get_entity ( entity) 
352+             . expect ( "Could not find entity" ) 
353+             . get :: < Aabb > ( ) 
354+             . expect ( "Could not find initial AABB" ) ; 
355+ 
356+         let  mut  entity_ref = app
357+             . world 
358+             . get_entity_mut ( entity) 
359+             . expect ( "Could not find entity" ) ; 
360+         * entity_ref
361+             . get_mut :: < Text > ( ) 
362+             . expect ( "Missing Text on entity" )  = Text :: from_section ( SECOND_TEXT ,  default ( ) ) ; 
363+ 
364+         // Recomputes the AABB. 
365+         app. update ( ) ; 
366+ 
367+         let  second_aabb = * app
368+             . world 
369+             . get_entity ( entity) 
370+             . expect ( "Could not find entity" ) 
371+             . get :: < Aabb > ( ) 
372+             . expect ( "Could not find second AABB" ) ; 
373+ 
374+         // Check that the height is the same, but the width is greater. 
375+         approx:: assert_abs_diff_eq!( first_aabb. half_extents. y,  second_aabb. half_extents. y) ; 
376+         assert ! ( FIRST_TEXT . len( )  < SECOND_TEXT . len( ) ) ; 
377+         assert ! ( first_aabb. half_extents. x < second_aabb. half_extents. x) ; 
378+     } 
379+ } 
0 commit comments