1+ from array import array
2+
13import arcade
2- import arcade .gl as gl
4+ from arcade .gl import Buffer , BufferDescription , Geometry , Program
5+ from arcade .math import Vec2
36from arcade .texture_atlas .base import TextureAtlasBase
7+ from arcade .types import Rect
48
59
610class NinePatchTexture :
@@ -74,6 +78,7 @@ def __init__(
7478 self ._initialized = False
7579 self ._texture = texture
7680 self ._custom_atlas = atlas
81+ self ._geometry_cache : tuple [int , int , int , int , Rect ] | None = None
7782
7883 # pixel texture co-ordinate start and end of central box.
7984 self ._left = left
@@ -84,37 +89,16 @@ def __init__(
8489 self ._check_sizes ()
8590
8691 # Created in _init_deferred
87- self ._program : gl .program .Program
88- self ._geometry : gl .Geometry
92+ self ._buffer : Buffer
93+ self ._program : Program
94+ self ._geometry : Geometry
8995 self ._ctx : arcade .ArcadeContext
9096 self ._atlas : TextureAtlasBase
9197 try :
9298 self ._init_deferred ()
9399 except Exception :
94100 pass
95101
96- def _init_deferred (self ):
97- """Deferred initialization when lazy loaded"""
98- self ._ctx = arcade .get_window ().ctx
99- # TODO: Cache in context?
100- self ._program = self ._ctx .load_program (
101- vertex_shader = ":system:shaders/gui/nine_patch_vs.glsl" ,
102- geometry_shader = ":system:shaders/gui/nine_patch_gs.glsl" ,
103- fragment_shader = ":system:shaders/gui/nine_patch_fs.glsl" ,
104- )
105- # Configure texture channels
106- self ._program .set_uniform_safe ("uv_texture" , 0 )
107- self ._program ["sprite_texture" ] = 1
108-
109- # TODO: Cache in context?
110- self ._geometry = self ._ctx .geometry ()
111-
112- # References for the texture
113- self ._atlas = self ._custom_atlas or self ._ctx .default_atlas
114- self ._add_to_atlas (self .texture )
115-
116- self ._initialized = True
117-
118102 def initialize (self ) -> None :
119103 """
120104 Manually initialize the NinePatchTexture if it was lazy loaded.
@@ -141,7 +125,7 @@ def texture(self, texture: arcade.Texture):
141125 self ._add_to_atlas (texture )
142126
143127 @property
144- def program (self ) -> gl . program . Program :
128+ def program (self ) -> Program :
145129 """Get or set the shader program.
146130
147131 Returns the default shader if no other shader is assigned.
@@ -152,7 +136,7 @@ def program(self) -> gl.program.Program:
152136 return self ._program
153137
154138 @program .setter
155- def program (self , program : gl . program . Program ):
139+ def program (self , program : Program ):
156140 if not self ._initialized :
157141 raise RuntimeError ("The NinePatchTexture has not been initialized" )
158142
@@ -241,26 +225,22 @@ def draw_rect(
241225 if not self ._initialized :
242226 self ._init_deferred ()
243227
228+ self ._create_geometry (rect )
229+
244230 if blend :
245231 self ._ctx .enable_only (self ._ctx .BLEND )
246232 else :
247233 self ._ctx .disable (self ._ctx .BLEND )
248234
249- self .program .set_uniform_safe ("texture_id" , self ._atlas .get_texture_id (self ._texture ))
250235 if pixelated :
251236 self ._atlas .texture .filter = self ._ctx .NEAREST , self ._ctx .NEAREST
252237 else :
253238 self ._atlas .texture .filter = self ._ctx .LINEAR , self ._ctx .LINEAR
254239
255- self .program ["position" ] = rect .bottom_left
256- self .program ["start" ] = self ._left , self ._bottom
257- self .program ["end" ] = self .width - self ._right , self .height - self ._top
258- self .program ["size" ] = rect .size
259- self .program ["t_size" ] = self ._texture .size
260-
261240 self ._atlas .use_uv_texture (0 )
262241 self ._atlas .texture .use (1 )
263- self ._geometry .render (self ._program , vertices = 1 )
242+
243+ self ._geometry .render (self ._program )
264244
265245 if blend :
266246 self ._ctx .disable (self ._ctx .BLEND )
@@ -282,3 +262,218 @@ def _check_sizes(self):
282262 raise ValueError ("Left and right border must be smaller than texture width" )
283263 if self ._bottom + self ._top > self ._texture .height :
284264 raise ValueError ("Bottom and top border must be smaller than texture height" )
265+
266+ def _init_deferred (self ):
267+ """Deferred initialization when lazy loaded"""
268+ self ._ctx = arcade .get_window ().ctx
269+ # TODO: Cache in context?
270+ self ._program = self ._ctx .load_program (
271+ vertex_shader = ":system:shaders/gui/nine_patch_vs.glsl" ,
272+ fragment_shader = ":system:shaders/gui/nine_patch_fs.glsl" ,
273+ )
274+ # Configure texture channels
275+ self ._program .set_uniform_safe ("uv_texture" , 0 )
276+ self ._program ["sprite_texture" ] = 1
277+
278+ # 4 byte floats * 4 floats * 4 vertices * 9 patches
279+ self ._buffer = self ._ctx .buffer (reserve = 576 )
280+ # fmt: off
281+ self ._ibo = self ._ctx .buffer (
282+ data = array ("i" ,
283+ [
284+ # Triangulate the patches
285+ # First rot
286+ 0 , 1 , 2 ,
287+ 3 , 1 , 2 ,
288+
289+ 4 , 5 , 6 ,
290+ 7 , 5 , 6 ,
291+
292+ 8 , 9 , 10 ,
293+ 11 , 9 , 10 ,
294+
295+ # Middle row
296+ 12 , 13 , 14 ,
297+ 15 , 13 , 14 ,
298+
299+ 16 , 17 , 18 ,
300+ 19 , 17 , 18 ,
301+
302+ 20 , 21 , 22 ,
303+ 23 , 21 , 22 ,
304+
305+ # Bottom row
306+ 24 , 25 , 26 ,
307+ 27 , 25 , 26 ,
308+
309+ 28 , 29 , 30 ,
310+ 31 , 29 , 30 ,
311+
312+ 32 , 33 , 34 ,
313+ 35 , 33 , 34 ,
314+ ]
315+ ),
316+ )
317+ # fmt: on
318+ self ._geometry = self ._ctx .geometry (
319+ content = [BufferDescription (self ._buffer , "2f 2f" , ["in_position" , "in_uv" ])],
320+ index_buffer = self ._ibo ,
321+ mode = self ._ctx .TRIANGLES ,
322+ index_element_size = 4 ,
323+ )
324+
325+ # References for the texture
326+ self ._atlas = self ._custom_atlas or self ._ctx .default_atlas
327+ self ._add_to_atlas (self .texture )
328+
329+ # NOTE: Important to create geometry after the texture is added to the atlas
330+ # self._create_geometry(LBWH(0, 0, self.width, self.height))
331+ self ._initialized = True
332+
333+ def _create_geometry (self , rect : Rect ):
334+ """Create vertices for the 9-patch texture."""
335+ # NOTE: This was ported from glsl geometry shader to python
336+ # Simulate old uniforms
337+ cache_key = (self ._left , self ._right , self ._bottom , self ._top , rect )
338+ if cache_key == self ._geometry_cache :
339+ return
340+ self ._geometry_cache = cache_key
341+
342+ position = rect .bottom_left
343+ start = Vec2 (self ._left , self ._bottom )
344+ end = Vec2 (self .width - self ._right , self .height - self ._top )
345+ size = rect .size
346+ t_size = Vec2 (* self ._texture .size )
347+ atlas_size = Vec2 (* self ._atlas .size )
348+
349+ # Patch points starting from upper left row by row
350+ p1 = position + Vec2 (0.0 , size .y )
351+ p2 = position + Vec2 (start .x , size .y )
352+ p3 = position + Vec2 (size .x - (t_size .x - end .x ), size .y )
353+ p4 = position + Vec2 (size .x , size .y )
354+
355+ y = size .y - (t_size .y - end .y )
356+ p5 = position + Vec2 (0.0 , y )
357+ p6 = position + Vec2 (start .x , y )
358+ p7 = position + Vec2 (size .x - (t_size .x - end .x ), y )
359+ p8 = position + Vec2 (size .x , y )
360+
361+ p9 = position + Vec2 (0.0 , start .y )
362+ p10 = position + Vec2 (start .x , start .y )
363+ p11 = position + Vec2 (size .x - (t_size .x - end .x ), start .y )
364+ p12 = position + Vec2 (size .x , start .y )
365+
366+ p13 = position + Vec2 (0.0 , 0.0 )
367+ p14 = position + Vec2 (start .x , 0.0 )
368+ p15 = position + Vec2 (size .x - (t_size .x - end .x ), 0.0 )
369+ p16 = position + Vec2 (size .x , 0.0 )
370+
371+ # <AtlasRegion
372+ # x=1 y=1
373+ # width=100 height=100
374+ # uvs=(
375+ # 0.001953125, 0.001953125,
376+ # 0.197265625, 0.001953125,
377+ # 0.001953125, 0.197265625,
378+ # 0.197265625, 0.197265625,
379+ # )
380+ # Get texture coordinates
381+ # vec2 uv0, uv1, uv2, uv3
382+ region = self ._atlas .get_texture_region_info (self ._texture .atlas_name )
383+ tex_coords = region .texture_coordinates
384+ uv0 = Vec2 (tex_coords [0 ], tex_coords [1 ])
385+ uv1 = Vec2 (tex_coords [2 ], tex_coords [3 ])
386+ uv2 = Vec2 (tex_coords [4 ], tex_coords [5 ])
387+ uv3 = Vec2 (tex_coords [6 ], tex_coords [7 ])
388+
389+ # Local corner offsets in pixels
390+ left = start .x
391+ right = t_size .x - end .x
392+ top = t_size .y - end .y
393+ bottom = start .y
394+
395+ # UV offsets to the inner rectangle in the patch
396+ # This is the global texture coordinate offset in the entire atlas
397+ c1 = Vec2 (left , top ) / atlas_size # Upper left corner
398+ c2 = Vec2 (right , top ) / atlas_size # Upper right corner
399+ c3 = Vec2 (left , bottom ) / atlas_size # Lower left corner
400+ c4 = Vec2 (right , bottom ) / atlas_size # Lower right corner
401+
402+ # Texture coordinates for all the points in the patch
403+ t1 = uv0
404+ t2 = uv0 + Vec2 (c1 .x , 0.0 )
405+ t3 = uv1 - Vec2 (c2 .x , 0.0 )
406+ t4 = uv1
407+
408+ t5 = uv0 + Vec2 (0.0 , c1 .y )
409+ t6 = uv0 + c1
410+ t7 = uv1 + Vec2 (- c2 .x , c2 .y )
411+ t8 = uv1 + Vec2 (0.0 , c2 .y )
412+
413+ t9 = uv2 - Vec2 (0.0 , c3 .y )
414+ t10 = uv2 + Vec2 (c3 .x , - c3 .y )
415+ t11 = uv3 - c4
416+ t12 = uv3 - Vec2 (0.0 , c4 .y )
417+
418+ t13 = uv2
419+ t14 = uv2 + Vec2 (c3 .x , 0.0 )
420+ t15 = uv3 - Vec2 (c4 .x , 0.0 )
421+ t16 = uv3
422+
423+ # fmt: off
424+ primitives = [
425+ # First row - two fixed corners + stretchy middle
426+ # Upper left corner. Fixed size.
427+ p1 , t1 ,
428+ p5 , t5 ,
429+ p2 , t2 ,
430+ p6 , t6 ,
431+ # Upper middle part stretches on x axis
432+ p2 , t2 ,
433+ p6 , t6 ,
434+ p3 , t3 ,
435+ p7 , t7 ,
436+ # Upper right corner. Fixed size
437+ p3 , t3 ,
438+ p7 , t7 ,
439+ p4 , t4 ,
440+ p8 , t8 ,
441+
442+ # Middle row: Two stretchy sides + stretchy middle
443+ # left border sketching on y axis
444+ p5 , t5 ,
445+ p9 , t9 ,
446+ p6 , t6 ,
447+ p10 , t10 ,
448+ # Center stretchy area
449+ p6 , t6 ,
450+ p10 , t10 ,
451+ p7 , t7 ,
452+ p11 , t11 ,
453+ # Right border. Stenches on y axis
454+ p7 , t7 ,
455+ p11 , t11 ,
456+ p8 , t8 ,
457+ p12 , t12 ,
458+
459+ # Bottom row: two fixed corners + stretchy middle
460+ # Lower left corner. Fixed size.
461+ p9 , t9 ,
462+ p13 , t13 ,
463+ p10 , t10 ,
464+ p14 , t14 ,
465+ # Lower middle part stretches on x axis
466+ p10 , t10 ,
467+ p14 , t14 ,
468+ p11 , t11 ,
469+ p15 , t15 ,
470+ # Lower right corner. Fixed size
471+ p11 , t11 ,
472+ p15 , t15 ,
473+ p12 , t12 ,
474+ p16 , t16 ,
475+ ]
476+ # fmt: on
477+
478+ data = array ("f" , [coord for point in primitives for coord in point ])
479+ self ._buffer .write (data .tobytes ())
0 commit comments