pyglet 2.1.12__py3-none-any.whl → 3.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. pyglet/__init__.py +67 -61
  2. pyglet/__init__.pyi +15 -8
  3. pyglet/app/__init__.py +22 -13
  4. pyglet/app/async_app.py +212 -0
  5. pyglet/app/base.py +2 -1
  6. pyglet/app/{xlib.py → linux.py} +3 -3
  7. pyglet/config/__init__.py +101 -0
  8. pyglet/config/gl/__init__.py +30 -0
  9. pyglet/config/gl/egl.py +120 -0
  10. pyglet/config/gl/macos.py +262 -0
  11. pyglet/config/gl/windows.py +267 -0
  12. pyglet/config/gl/x11.py +142 -0
  13. pyglet/customtypes.py +43 -2
  14. pyglet/display/__init__.py +8 -6
  15. pyglet/display/base.py +3 -63
  16. pyglet/display/cocoa.py +12 -17
  17. pyglet/display/emscripten.py +39 -0
  18. pyglet/display/headless.py +23 -30
  19. pyglet/display/wayland.py +157 -0
  20. pyglet/display/win32.py +4 -17
  21. pyglet/display/xlib.py +19 -27
  22. pyglet/display/xlib_vidmoderestore.py +2 -2
  23. pyglet/enums.py +183 -0
  24. pyglet/event.py +0 -1
  25. pyglet/experimental/geoshader_sprite.py +15 -13
  26. pyglet/experimental/hidraw.py +6 -15
  27. pyglet/experimental/multitexture_sprite.py +31 -19
  28. pyglet/experimental/particles.py +13 -35
  29. pyglet/font/__init__.py +251 -85
  30. pyglet/font/base.py +116 -61
  31. pyglet/font/dwrite/__init__.py +349 -204
  32. pyglet/font/dwrite/dwrite_lib.py +27 -5
  33. pyglet/font/fontconfig.py +14 -6
  34. pyglet/font/freetype.py +138 -87
  35. pyglet/font/freetype_lib.py +19 -0
  36. pyglet/font/group.py +179 -0
  37. pyglet/font/harfbuzz/__init__.py +3 -3
  38. pyglet/font/pyodide_js.py +310 -0
  39. pyglet/font/quartz.py +319 -126
  40. pyglet/font/ttf.py +45 -3
  41. pyglet/font/user.py +14 -19
  42. pyglet/font/win32.py +45 -21
  43. pyglet/graphics/__init__.py +8 -787
  44. pyglet/graphics/allocation.py +115 -1
  45. pyglet/graphics/api/__init__.py +77 -0
  46. pyglet/graphics/api/base.py +299 -0
  47. pyglet/graphics/api/gl/__init__.py +58 -0
  48. pyglet/graphics/api/gl/base.py +24 -0
  49. pyglet/graphics/{vertexbuffer.py → api/gl/buffer.py} +104 -159
  50. pyglet/graphics/api/gl/cocoa/context.py +76 -0
  51. pyglet/graphics/api/gl/context.py +391 -0
  52. pyglet/graphics/api/gl/default_shaders.py +0 -0
  53. pyglet/graphics/api/gl/draw.py +627 -0
  54. pyglet/graphics/api/gl/egl/__init__.py +0 -0
  55. pyglet/graphics/api/gl/egl/context.py +92 -0
  56. pyglet/graphics/api/gl/enums.py +76 -0
  57. pyglet/graphics/api/gl/framebuffer.py +315 -0
  58. pyglet/graphics/api/gl/gl.py +5463 -0
  59. pyglet/graphics/api/gl/gl_info.py +188 -0
  60. pyglet/graphics/api/gl/global_opengl.py +226 -0
  61. pyglet/{gl → graphics/api/gl}/lib.py +34 -18
  62. pyglet/graphics/api/gl/shader.py +1476 -0
  63. pyglet/graphics/api/gl/shapes.py +55 -0
  64. pyglet/graphics/api/gl/sprite.py +102 -0
  65. pyglet/graphics/api/gl/state.py +219 -0
  66. pyglet/graphics/api/gl/text.py +190 -0
  67. pyglet/graphics/api/gl/texture.py +1526 -0
  68. pyglet/graphics/{vertexarray.py → api/gl/vertexarray.py} +11 -13
  69. pyglet/graphics/api/gl/vertexdomain.py +751 -0
  70. pyglet/graphics/api/gl/win32/__init__.py +0 -0
  71. pyglet/graphics/api/gl/win32/context.py +108 -0
  72. pyglet/graphics/api/gl/win32/wgl_info.py +24 -0
  73. pyglet/graphics/api/gl/xlib/__init__.py +0 -0
  74. pyglet/graphics/api/gl/xlib/context.py +174 -0
  75. pyglet/{gl → graphics/api/gl/xlib}/glx_info.py +26 -31
  76. pyglet/graphics/api/gl1/__init__.py +0 -0
  77. pyglet/{gl → graphics/api/gl1}/gl_compat.py +3 -2
  78. pyglet/graphics/api/gl2/__init__.py +0 -0
  79. pyglet/graphics/api/gl2/buffer.py +320 -0
  80. pyglet/graphics/api/gl2/draw.py +600 -0
  81. pyglet/graphics/api/gl2/global_opengl.py +122 -0
  82. pyglet/graphics/api/gl2/shader.py +200 -0
  83. pyglet/graphics/api/gl2/shapes.py +51 -0
  84. pyglet/graphics/api/gl2/sprite.py +79 -0
  85. pyglet/graphics/api/gl2/text.py +175 -0
  86. pyglet/graphics/api/gl2/vertexdomain.py +364 -0
  87. pyglet/graphics/api/webgl/__init__.py +233 -0
  88. pyglet/graphics/api/webgl/buffer.py +302 -0
  89. pyglet/graphics/api/webgl/context.py +234 -0
  90. pyglet/graphics/api/webgl/draw.py +590 -0
  91. pyglet/graphics/api/webgl/enums.py +76 -0
  92. pyglet/graphics/api/webgl/framebuffer.py +360 -0
  93. pyglet/graphics/api/webgl/gl.py +1537 -0
  94. pyglet/graphics/api/webgl/gl_info.py +130 -0
  95. pyglet/graphics/api/webgl/shader.py +1346 -0
  96. pyglet/graphics/api/webgl/shapes.py +92 -0
  97. pyglet/graphics/api/webgl/sprite.py +102 -0
  98. pyglet/graphics/api/webgl/state.py +227 -0
  99. pyglet/graphics/api/webgl/text.py +187 -0
  100. pyglet/graphics/api/webgl/texture.py +1227 -0
  101. pyglet/graphics/api/webgl/vertexarray.py +54 -0
  102. pyglet/graphics/api/webgl/vertexdomain.py +616 -0
  103. pyglet/graphics/api/webgl/webgl_js.pyi +307 -0
  104. pyglet/{image → graphics}/atlas.py +33 -32
  105. pyglet/graphics/base.py +10 -0
  106. pyglet/graphics/buffer.py +245 -0
  107. pyglet/graphics/draw.py +578 -0
  108. pyglet/graphics/framebuffer.py +26 -0
  109. pyglet/graphics/instance.py +178 -69
  110. pyglet/graphics/shader.py +267 -1553
  111. pyglet/graphics/state.py +83 -0
  112. pyglet/graphics/texture.py +703 -0
  113. pyglet/graphics/vertexdomain.py +695 -538
  114. pyglet/gui/ninepatch.py +10 -10
  115. pyglet/gui/widgets.py +120 -10
  116. pyglet/image/__init__.py +20 -1973
  117. pyglet/image/animation.py +12 -12
  118. pyglet/image/base.py +730 -0
  119. pyglet/image/codecs/__init__.py +9 -0
  120. pyglet/image/codecs/bmp.py +53 -30
  121. pyglet/image/codecs/dds.py +53 -31
  122. pyglet/image/codecs/gdiplus.py +38 -14
  123. pyglet/image/codecs/gdkpixbuf2.py +0 -2
  124. pyglet/image/codecs/js_image.py +99 -0
  125. pyglet/image/codecs/ktx2.py +161 -0
  126. pyglet/image/codecs/pil.py +1 -1
  127. pyglet/image/codecs/png.py +1 -1
  128. pyglet/image/codecs/wic.py +11 -2
  129. pyglet/info.py +26 -24
  130. pyglet/input/__init__.py +8 -0
  131. pyglet/input/base.py +163 -105
  132. pyglet/input/controller.py +13 -19
  133. pyglet/input/controller_db.py +39 -24
  134. pyglet/input/emscripten/__init__.py +18 -0
  135. pyglet/input/emscripten/gamepad_js.py +397 -0
  136. pyglet/input/linux/__init__.py +11 -5
  137. pyglet/input/linux/evdev.py +10 -11
  138. pyglet/input/linux/x11_xinput.py +2 -2
  139. pyglet/input/linux/x11_xinput_tablet.py +1 -1
  140. pyglet/input/macos/__init__.py +7 -2
  141. pyglet/input/macos/darwin_gc.py +559 -0
  142. pyglet/input/win32/__init__.py +1 -1
  143. pyglet/input/win32/directinput.py +34 -29
  144. pyglet/input/win32/xinput.py +11 -61
  145. pyglet/lib.py +3 -3
  146. pyglet/libs/__init__.py +1 -1
  147. pyglet/{gl → libs/darwin}/agl.py +1 -1
  148. pyglet/libs/darwin/cocoapy/__init__.py +2 -2
  149. pyglet/libs/darwin/cocoapy/cocoahelpers.py +181 -0
  150. pyglet/libs/darwin/cocoapy/cocoalibs.py +31 -0
  151. pyglet/libs/darwin/cocoapy/cocoatypes.py +27 -0
  152. pyglet/libs/darwin/cocoapy/runtime.py +81 -45
  153. pyglet/libs/darwin/coreaudio.py +4 -4
  154. pyglet/{gl → libs/darwin}/lib_agl.py +9 -8
  155. pyglet/libs/darwin/quartzkey.py +1 -3
  156. pyglet/libs/egl/__init__.py +2 -0
  157. pyglet/libs/egl/egl_lib.py +576 -0
  158. pyglet/libs/egl/eglext.py +51 -5
  159. pyglet/libs/linux/__init__.py +0 -0
  160. pyglet/libs/linux/egl/__init__.py +0 -0
  161. pyglet/libs/linux/egl/eglext.py +22 -0
  162. pyglet/libs/linux/glx/__init__.py +0 -0
  163. pyglet/{gl → libs/linux/glx}/glx.py +13 -14
  164. pyglet/{gl → libs/linux/glx}/glxext_arb.py +408 -192
  165. pyglet/{gl → libs/linux/glx}/glxext_mesa.py +1 -1
  166. pyglet/{gl → libs/linux/glx}/glxext_nv.py +345 -164
  167. pyglet/{gl → libs/linux/glx}/lib_glx.py +3 -2
  168. pyglet/libs/linux/wayland/__init__.py +0 -0
  169. pyglet/libs/linux/wayland/client.py +1068 -0
  170. pyglet/libs/linux/wayland/lib_wayland.py +207 -0
  171. pyglet/libs/linux/wayland/wayland_egl.py +38 -0
  172. pyglet/libs/{wayland → linux/wayland}/xkbcommon.py +26 -0
  173. pyglet/libs/{x11 → linux/x11}/xf86vmode.py +4 -4
  174. pyglet/libs/{x11 → linux/x11}/xinerama.py +2 -2
  175. pyglet/libs/{x11 → linux/x11}/xinput.py +10 -10
  176. pyglet/libs/linux/x11/xrandr.py +0 -0
  177. pyglet/libs/{x11 → linux/x11}/xrender.py +1 -1
  178. pyglet/libs/shared/__init__.py +0 -0
  179. pyglet/libs/shared/spirv/__init__.py +0 -0
  180. pyglet/libs/shared/spirv/lib_shaderc.py +85 -0
  181. pyglet/libs/shared/spirv/lib_spirv_cross.py +126 -0
  182. pyglet/libs/win32/__init__.py +27 -5
  183. pyglet/libs/win32/constants.py +59 -48
  184. pyglet/libs/win32/context_managers.py +20 -3
  185. pyglet/libs/win32/dinput.py +105 -88
  186. pyglet/{gl → libs/win32}/lib_wgl.py +52 -26
  187. pyglet/libs/win32/types.py +58 -23
  188. pyglet/{gl → libs/win32}/wgl.py +32 -25
  189. pyglet/{gl → libs/win32}/wglext_arb.py +364 -2
  190. pyglet/media/__init__.py +9 -10
  191. pyglet/media/codecs/__init__.py +12 -1
  192. pyglet/media/codecs/base.py +99 -96
  193. pyglet/media/codecs/ffmpeg.py +2 -2
  194. pyglet/media/codecs/ffmpeg_lib/libavformat.py +3 -8
  195. pyglet/media/codecs/webaudio_pyodide.py +111 -0
  196. pyglet/media/drivers/__init__.py +9 -4
  197. pyglet/media/drivers/base.py +4 -4
  198. pyglet/media/drivers/openal/__init__.py +1 -1
  199. pyglet/media/drivers/openal/adaptation.py +3 -3
  200. pyglet/media/drivers/pulse/__init__.py +1 -1
  201. pyglet/media/drivers/pulse/adaptation.py +3 -3
  202. pyglet/media/drivers/pyodide_js/__init__.py +8 -0
  203. pyglet/media/drivers/pyodide_js/adaptation.py +288 -0
  204. pyglet/media/drivers/xaudio2/adaptation.py +3 -3
  205. pyglet/media/player.py +276 -193
  206. pyglet/media/player_worker_thread.py +1 -1
  207. pyglet/model/__init__.py +39 -29
  208. pyglet/model/codecs/base.py +4 -4
  209. pyglet/model/codecs/gltf.py +3 -3
  210. pyglet/model/codecs/obj.py +71 -43
  211. pyglet/resource.py +129 -78
  212. pyglet/shapes.py +147 -177
  213. pyglet/sprite.py +47 -164
  214. pyglet/text/__init__.py +44 -54
  215. pyglet/text/caret.py +12 -7
  216. pyglet/text/document.py +19 -17
  217. pyglet/text/formats/html.py +2 -2
  218. pyglet/text/formats/structured.py +10 -40
  219. pyglet/text/layout/__init__.py +20 -13
  220. pyglet/text/layout/base.py +176 -287
  221. pyglet/text/layout/incremental.py +9 -10
  222. pyglet/text/layout/scrolling.py +7 -95
  223. pyglet/window/__init__.py +183 -172
  224. pyglet/window/cocoa/__init__.py +62 -51
  225. pyglet/window/cocoa/pyglet_delegate.py +2 -25
  226. pyglet/window/cocoa/pyglet_view.py +9 -8
  227. pyglet/window/dialog/__init__.py +184 -0
  228. pyglet/window/dialog/base.py +99 -0
  229. pyglet/window/dialog/darwin.py +121 -0
  230. pyglet/window/dialog/linux.py +72 -0
  231. pyglet/window/dialog/windows.py +194 -0
  232. pyglet/window/emscripten/__init__.py +779 -0
  233. pyglet/window/headless/__init__.py +44 -28
  234. pyglet/window/key.py +2 -0
  235. pyglet/window/mouse.py +2 -2
  236. pyglet/window/wayland/__init__.py +377 -0
  237. pyglet/window/win32/__init__.py +101 -46
  238. pyglet/window/xlib/__init__.py +104 -66
  239. {pyglet-2.1.12.dist-info → pyglet-3.0.dev1.dist-info}/METADATA +2 -3
  240. pyglet-3.0.dev1.dist-info/RECORD +322 -0
  241. {pyglet-2.1.12.dist-info → pyglet-3.0.dev1.dist-info}/WHEEL +1 -1
  242. pyglet/gl/__init__.py +0 -208
  243. pyglet/gl/base.py +0 -499
  244. pyglet/gl/cocoa.py +0 -309
  245. pyglet/gl/gl.py +0 -4625
  246. pyglet/gl/gl.pyi +0 -2320
  247. pyglet/gl/gl_compat.pyi +0 -3097
  248. pyglet/gl/gl_info.py +0 -190
  249. pyglet/gl/headless.py +0 -166
  250. pyglet/gl/wgl_info.py +0 -36
  251. pyglet/gl/wglext_nv.py +0 -1096
  252. pyglet/gl/win32.py +0 -268
  253. pyglet/gl/xlib.py +0 -295
  254. pyglet/image/buffer.py +0 -274
  255. pyglet/image/codecs/s3tc.py +0 -354
  256. pyglet/libs/x11/xrandr.py +0 -166
  257. pyglet-2.1.12.dist-info/RECORD +0 -234
  258. /pyglet/{libs/wayland → graphics/api/gl/cocoa}/__init__.py +0 -0
  259. /pyglet/libs/{egl → linux/egl}/egl.py +0 -0
  260. /pyglet/libs/{egl → linux/egl}/lib.py +0 -0
  261. /pyglet/libs/{ioctl.py → linux/ioctl.py} +0 -0
  262. /pyglet/libs/{wayland → linux/wayland}/gbm.py +0 -0
  263. /pyglet/libs/{x11 → linux/x11}/__init__.py +0 -0
  264. /pyglet/libs/{x11 → linux/x11}/cursorfont.py +0 -0
  265. /pyglet/libs/{x11 → linux/x11}/xlib.py +0 -0
  266. /pyglet/libs/{x11 → linux/x11}/xsync.py +0 -0
  267. {pyglet-2.1.12.dist-info/licenses → pyglet-3.0.dev1.dist-info}/LICENSE +0 -0
@@ -0,0 +1,1227 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Callable, Iterator, Literal, Union
4
+
5
+ import js
6
+ import pyodide
7
+
8
+ import pyglet
9
+ from pyglet.enums import TextureFilter, TextureType, ComponentFormat, \
10
+ AddressMode
11
+ from pyglet.graphics.api.webgl.enums import texture_map
12
+ from pyglet.graphics.api.webgl.gl import (
13
+ GL_BGR,
14
+ GL_BGR_INTEGER,
15
+ GL_BGRA,
16
+ GL_BGRA_INTEGER,
17
+ GL_COLOR_ATTACHMENT0,
18
+ GL_DEPTH_COMPONENT,
19
+ GL_DEPTH_STENCIL,
20
+ GL_FRAMEBUFFER,
21
+ GL_FRAMEBUFFER_COMPLETE,
22
+ GL_LINEAR_MIPMAP_LINEAR,
23
+ GL_PACK_ALIGNMENT,
24
+ GL_READ_WRITE,
25
+ GL_RED,
26
+ GL_RED_INTEGER,
27
+ GL_RG,
28
+ GL_RG_INTEGER,
29
+ GL_RGB,
30
+ GL_RGB_INTEGER,
31
+ GL_RGBA,
32
+ GL_RGBA8, # noqa: F401
33
+ GL_RGBA32F,
34
+ GL_RGBA_INTEGER,
35
+ GL_TEXTURE0,
36
+ GL_TEXTURE_2D,
37
+ GL_TEXTURE_2D_ARRAY,
38
+ GL_TEXTURE_3D,
39
+ GL_TEXTURE_MAG_FILTER,
40
+ GL_TEXTURE_MIN_FILTER,
41
+ GL_UNPACK_ALIGNMENT,
42
+ GL_UNPACK_ROW_LENGTH,
43
+ GL_UNPACK_SKIP_PIXELS,
44
+ GL_UNPACK_SKIP_ROWS,
45
+ GL_UNSIGNED_BYTE,
46
+ )
47
+ from pyglet.graphics.texture import TextureBase, TextureRegionBase, UniformTextureSequence, TextureArraySizeExceeded, \
48
+ TextureArrayDepthExceeded
49
+ from pyglet.image.base import (
50
+ CompressedImageData,
51
+ ImageData,
52
+ ImageDataRegion,
53
+ ImageException,
54
+ ImageGrid,
55
+ T,
56
+ _AbstractGrid,
57
+ _AbstractImage,
58
+ )
59
+
60
+ # from pyglet.image.buffer import Framebuffer, Renderbuffer, get_max_color_attachments
61
+
62
+ _api_base_internal_formats = {
63
+ 'R': 'GL_R',
64
+ 'RG': 'GL_RG',
65
+ 'RGB': 'GL_RGB',
66
+ 'RGBA': 'GL_RGBA',
67
+ 'BGR': 'GL_RGB',
68
+ 'BGRA': 'GL_RGBA',
69
+ 'D': 'GL_DEPTH_COMPONENT',
70
+ 'DS': 'GL_DEPTH_STENCIL',
71
+ }
72
+
73
+ _api_base_pixel_formats = {
74
+ 'R': 'GL_RED',
75
+ 'RG': 'GL_RG',
76
+ 'RGB': 'GL_RGB',
77
+ 'RGBA': 'GL_RGBA',
78
+ 'D': 'GL_DEPTH_COMPONENT',
79
+ 'DS': 'GL_DEPTH_STENCIL',
80
+ }
81
+
82
+
83
+ def _get_base_format(component_format: ComponentFormat) -> int:
84
+ return globals()[_api_base_pixel_formats[component_format]]
85
+
86
+
87
+ def _get_internal_format(component_format: ComponentFormat, bit_size: int = 8, data_type: str = "B") -> int:
88
+ """Convert our internal format class to the GL equivalent with size and type."""
89
+ # Base format based on components
90
+ base_format = _api_base_internal_formats.get(component_format.upper())
91
+
92
+ if base_format is None:
93
+ raise ValueError(f"Unknown format: {component_format}")
94
+
95
+ # Type suffix based on data type (integer, float, or default)
96
+ if data_type == "I":
97
+ type_suffix = "UI"
98
+ elif data_type == "i":
99
+ type_suffix = 'I'
100
+ elif data_type == "f":
101
+ type_suffix = 'F'
102
+ else:
103
+ type_suffix = '' # No suffix for unsigned normalized formats
104
+
105
+ # Construct the final GL format string.
106
+ # For example. Base_format: GL_RGBA, size: 32, "type": float -> GL_RGBA32F
107
+ gl_format = f"{base_format}{bit_size}{type_suffix}"
108
+
109
+ # Get the integer value of the GL constant using globals()
110
+ if gl_format in globals():
111
+ return globals()[gl_format]
112
+ raise ValueError(f"GL constant '{gl_format}' not defined.")
113
+
114
+
115
+ if TYPE_CHECKING:
116
+ from collections.abc import Iterator
117
+ from typing import Callable, Literal
118
+
119
+ from pyglet.graphics.api.webgl import OpenGLSurfaceContext
120
+ from pyglet.graphics.api.webgl.webgl_js import WebGL2RenderingContext
121
+
122
+ _api_pixel_formats = {
123
+ 'R': GL_RED,
124
+ 'RG': GL_RG,
125
+ 'RGB': GL_RGB,
126
+ 'BGR': GL_BGR,
127
+ 'RGBA': GL_RGBA,
128
+ 'BGRA': GL_BGRA,
129
+ 'RI': GL_RED_INTEGER,
130
+ 'RGI': GL_RG_INTEGER,
131
+ 'RGBI': GL_RGB_INTEGER,
132
+ 'BGRI': GL_BGR_INTEGER,
133
+ 'RGBAI': GL_RGBA_INTEGER,
134
+ 'BGRAI': GL_BGRA_INTEGER,
135
+ 'D': GL_DEPTH_COMPONENT,
136
+ 'DS': GL_DEPTH_STENCIL,
137
+ }
138
+
139
+
140
+ def get_max_texture_size() -> int:
141
+ """Query the maximum texture size available"""
142
+ return pyglet.graphics.api.core.current_context.get_info().MAX_ARRAY_TEXTURE_LAYERS
143
+
144
+
145
+ def get_max_array_texture_layers() -> int:
146
+ return pyglet.graphics.api.core.current_context.get_info().MAX_ARRAY_TEXTURE_LAYERS
147
+
148
+
149
+ def _get_gl_format_and_type(fmt: str):
150
+ fmt = _api_pixel_formats.get(fmt)
151
+ if fmt:
152
+ return fmt, GL_UNSIGNED_BYTE # Eventually support others through ImageData.
153
+
154
+ return None, None
155
+
156
+
157
+ class GLCompressedImageData(CompressedImageData):
158
+ """Compressed image data suitable for direct uploading to GPU."""
159
+
160
+ # TODO: Finish compressed.
161
+
162
+ _current_texture = None
163
+ _current_mipmap_texture = None
164
+
165
+ def __init__(
166
+ self,
167
+ width: int,
168
+ height: int,
169
+ gl_format: int,
170
+ data: bytes,
171
+ extension: str | None = None,
172
+ decoder: Callable[[bytes, int, int], _AbstractImage] | None = None,
173
+ ) -> None:
174
+ """Construct a CompressedImageData with the given compressed data.
175
+
176
+ Args:
177
+ width:
178
+ The width of the image.
179
+ height:
180
+ The height of the image.
181
+ gl_format:
182
+ GL constant giving the format of the compressed data.
183
+ For example: ``GL_COMPRESSED_RGBA_S3TC_DXT5_EXT``.
184
+ data:
185
+ An array of bytes containing the compressed image data.
186
+ extension:
187
+ If specified, gives the name of a GL extension to check for
188
+ before creating a texture.
189
+ decoder:
190
+ An optional fallback function used to decode the compressed data.
191
+ This function is called if the required extension is not present.
192
+ """
193
+ super().__init__(width, height)
194
+ self.data = data
195
+ self.gl_format = gl_format
196
+ self.extension = extension
197
+ self.decoder = decoder
198
+ self.mipmap_data = []
199
+
200
+ def set_mipmap_data(self, level: int, data: bytes) -> None:
201
+ """Set compressed image data for a mipmap level.
202
+
203
+ Supplied data gives a compressed image for the given mipmap level.
204
+ This image data must be in the same format as was used in the
205
+ constructor. The image data must also be of the correct dimensions for
206
+ the level (i.e., width >> level, height >> level); but this is not checked.
207
+ If *any* mipmap levels are specified, they are used; otherwise, mipmaps for
208
+ ``mipmapped_texture`` are generated automatically.
209
+ """
210
+ # Extend mipmap_data list to required level
211
+ self.mipmap_data += [None] * (level - len(self.mipmap_data))
212
+ self.mipmap_data[level - 1] = data
213
+
214
+ def _have_extension(self) -> bool:
215
+ from pyglet.graphics.api import core
216
+
217
+ return self.extension is None or core.have_extension(self.extension)
218
+
219
+ def get_texture(self) -> TextureBase:
220
+ if self._current_texture:
221
+ return self._current_texture
222
+
223
+ texture = TextureBase.create(self.width, self.height, blank_data=False)
224
+
225
+ if self.anchor_x or self.anchor_y:
226
+ texture.anchor_x = self.anchor_x
227
+ texture.anchor_y = self.anchor_y
228
+
229
+ gl = pyglet.graphics.api.core.current_context.gl
230
+ gl.bindTexture(texture.target, texture.id)
231
+ gl.texParameteri(texture.target, GL_TEXTURE_MIN_FILTER, texture.min_filter)
232
+ gl.texParameteri(texture.target, GL_TEXTURE_MAG_FILTER, texture.mag_filter)
233
+
234
+ if self._have_extension():
235
+ gl.compressedTexImage2D(
236
+ texture.target, texture.level, self.gl_format, self.width, self.height, 0, len(self.data), self.data,
237
+ )
238
+ elif self.decoder:
239
+ image = self.decoder(self.data, self.width, self.height)
240
+ texture = image.get_texture()
241
+ assert texture.width == self.width
242
+ assert texture.height == self.height
243
+ else:
244
+ msg = f"No extension or fallback decoder is available to decode {self}"
245
+ raise ImageException(msg)
246
+
247
+ gl.flush()
248
+ self._current_texture = texture
249
+ return texture
250
+
251
+ def get_mipmapped_texture(self) -> TextureBase:
252
+ if self._current_mipmap_texture:
253
+ return self._current_mipmap_texture
254
+
255
+ if not self._have_extension():
256
+ # TODO: mip-mapped software decoded compressed textures.
257
+ # For now, just return a non-mipmapped texture.
258
+ return self.get_texture()
259
+
260
+ texture = TextureBase.create(self.width, self.height, GL_TEXTURE_2D, None)
261
+
262
+ if self.anchor_x or self.anchor_y:
263
+ texture.anchor_x = self.anchor_x
264
+ texture.anchor_y = self.anchor_y
265
+
266
+ gl = pyglet.graphics.api.core.current_context.gl
267
+ gl.bindTexture(texture.target, texture.id)
268
+
269
+ gl.texParameteri(texture.target, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
270
+
271
+ if not self.mipmap_data:
272
+ gl.generateMipmap(texture.target)
273
+
274
+ gl.compressedTexImage2D(
275
+ texture.target, texture.level, self.gl_format, self.width, self.height, 0, len(self.data), self.data,
276
+ )
277
+
278
+ width, height = self.width, self.height
279
+ level = 0
280
+ for data in self.mipmap_data:
281
+ width >>= 1
282
+ height >>= 1
283
+ level += 1
284
+ gl.compressedTexImage2D(texture.target, level, self.gl_format, width, height, 0, len(data), data)
285
+
286
+ gl.flush()
287
+
288
+ self._current_mipmap_texture = texture
289
+ return texture
290
+
291
+ def blit_to_texture(self, target: int, level: int, x: int, y: int, z: int, internalformat: int = None):
292
+ if not self._have_extension():
293
+ raise ImageException(f"{self.extension} is required to decode {self}")
294
+
295
+ # TODO: use glCompressedTexImage2D/3D if `internalformat` is specified.
296
+
297
+ gl = pyglet.graphics.api.core.current_context.gl
298
+ if target == GL_TEXTURE_3D:
299
+ gl.compressedTexSubImage3D(
300
+ target,
301
+ level,
302
+ x - self.anchor_x,
303
+ y - self.anchor_y,
304
+ z,
305
+ self.width,
306
+ self.height,
307
+ 1,
308
+ self.gl_format,
309
+ len(self.data),
310
+ self.data,
311
+ )
312
+ else:
313
+ gl.compressedTexSubImage2D(
314
+ target,
315
+ level,
316
+ x - self.anchor_x,
317
+ y - self.anchor_y,
318
+ self.width,
319
+ self.height,
320
+ self.gl_format,
321
+ len(self.data),
322
+ self.data,
323
+ )
324
+
325
+ def get_image_data(self) -> CompressedImageData:
326
+ return self
327
+
328
+ def get_region(self, x: int, y: int, width: int, height: int) -> _AbstractImage:
329
+ raise NotImplementedError(f"Not implemented for {self}")
330
+
331
+ def blit(self, x: int, y: int, z: int = 0) -> None:
332
+ self.get_texture().blit(x, y, z)
333
+
334
+ def blit_into(self, source, x: int, y: int, z: int) -> None:
335
+ raise NotImplementedError(f"Not implemented for {self}")
336
+
337
+
338
+ def _get_pixel_format(image_data: ImageData) -> tuple[int, int]:
339
+ """Determine the pixel format from format string for the Graphics API."""
340
+ data_format = image_data.format
341
+ fmt, gl_type = _get_gl_format_and_type(data_format)
342
+
343
+ if fmt is None:
344
+ # Need to convert data to a standard form
345
+ data_format = {
346
+ 1: 'R',
347
+ 2: 'RG',
348
+ 3: 'RGB',
349
+ 4: 'RGBA',
350
+ }.get(len(data_format))
351
+ fmt, gl_type = _get_gl_format_and_type(data_format)
352
+
353
+ return fmt, gl_type
354
+
355
+
356
+ class Texture(TextureBase):
357
+ """An image loaded into GPU memory.
358
+
359
+ Typically, you will get an instance of Texture by accessing calling
360
+ the ``get_texture()`` method of any AbstractImage class (such as ImageData).
361
+ """
362
+
363
+ region_class: TextureRegion # Set to TextureRegion after it's defined
364
+ """The class to use when constructing regions of this texture.
365
+ The class should be a subclass of TextureRegion.
366
+ """
367
+
368
+ tex_coords = (0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0)
369
+ """12-tuple of float, named (u1, v1, r1, u2, v2, r2, ...).
370
+ ``u, v, r`` give the 3D texture coordinates for vertices 1-4. The vertices
371
+ are specified in the order bottom-left, bottom-right, top-right and top-left.
372
+ """
373
+
374
+ tex_coords_order: tuple[int, int, int, int] = (0, 1, 2, 3)
375
+ """The default vertex winding order for a quad.
376
+ This defaults to counter-clockwise, starting at the bottom-left.
377
+ """
378
+
379
+ # If this backend supports pixel data conversion.
380
+ # If False, will force data to be RGBA, even if CPU is used to order it.
381
+ pixel_conversion = True
382
+
383
+ level: int = 0
384
+ """The mipmap level of this texture."""
385
+
386
+ images = 1
387
+
388
+ default_filters: TextureFilter | tuple[TextureFilter, TextureFilter] = TextureFilter.LINEAR, TextureFilter.LINEAR
389
+ """The default minification and magnification filters, as a tuple.
390
+ Both default to LINEAR. If a texture is created without specifying
391
+ a filter, these defaults will be used.
392
+ """
393
+
394
+ x: int = 0
395
+ y: int = 0
396
+ z: int = 0
397
+
398
+ _ctx: OpenGLSurfaceContext
399
+ _gl: WebGL2RenderingContext
400
+
401
+ def __init__(self, context: OpenGLSurfaceContext, width: int, height: int, tex_id: int,
402
+ tex_type: TextureType = TextureType.TYPE_2D,
403
+ internal_format: ComponentFormat = ComponentFormat.RGBA,
404
+ internal_format_size: int = 8,
405
+ internal_format_type: str = "B",
406
+ filters: TextureFilter | tuple[TextureFilter, TextureFilter] | None = None,
407
+ address_mode: AddressMode = AddressMode.REPEAT,
408
+ anisotropic_level: int = 0):
409
+ super().__init__(width, height, tex_id, tex_type, internal_format, internal_format_size, internal_format_type,
410
+ filters, address_mode, anisotropic_level)
411
+ self._context = context
412
+ self._gl = self._context.gl
413
+ self.target = texture_map[self.tex_type]
414
+ self._gl_min_filter = texture_map[self.min_filter]
415
+ self._gl_mag_filter = texture_map[self.mag_filter]
416
+ self._gl_internal_format = _get_internal_format(internal_format, internal_format_size, internal_format_type)
417
+
418
+ def delete(self) -> None:
419
+ """Delete this texture and the memory it occupies.
420
+
421
+ Textures are invalid after deletion, and may no longer be used.
422
+ """
423
+ self._gl.deleteTexture(self.id)
424
+ self.id = None
425
+
426
+ def __del__(self):
427
+ if self.id is not None:
428
+ try:
429
+ self._context.delete_texture(self.id)
430
+ self.id = None
431
+ except (AttributeError, ImportError):
432
+ pass # Interpreter is shutting down
433
+
434
+ def bind(self, texture_unit: int = 0) -> None:
435
+ """Bind to a specific Texture Unit by number."""
436
+ self._gl.activeTexture(GL_TEXTURE0 + texture_unit)
437
+ self._gl.bindTexture(self.target, self.id)
438
+
439
+ def bind_image_texture(
440
+ self,
441
+ unit: int,
442
+ level: int = 0,
443
+ layered: bool = False,
444
+ layer: int = 0,
445
+ access: int = GL_READ_WRITE,
446
+ fmt: int = GL_RGBA32F,
447
+ ):
448
+ """Bind as an ImageTexture for use with a :py:class:`~pyglet.shader.ComputeShaderProgram`.
449
+
450
+ .. note:: OpenGL 4.3, or 4.2 with the GL_ARB_compute_shader extention is required.
451
+ """
452
+ raise NotImplementedError("Not supported.")
453
+
454
+ @staticmethod
455
+ def _get_image_alignment(image_data: ImageData) -> tuple[int, int]:
456
+ """Image alignment and row length information on an Image to upload.
457
+
458
+ Args:
459
+ image_data: The image data to get the alignment from.
460
+
461
+ Returns:
462
+ (align, row_length)
463
+ align: 1, 2, 4, or 8 \
464
+ row_length: 0 if tightly packed.
465
+ """
466
+ components = len(image_data.format)
467
+ width = image_data.width
468
+ packed_row_bytes = width * components
469
+ pitch = abs(image_data._current_pitch)
470
+ if packed_row_bytes % 8 == 0:
471
+ align = 8
472
+ elif packed_row_bytes % 4 == 0:
473
+ align = 4
474
+ elif packed_row_bytes % 2 == 0:
475
+ align = 2
476
+ else:
477
+ align = 1
478
+
479
+ if pitch == packed_row_bytes:
480
+ row_length = 0
481
+ else:
482
+ row_length = pitch // components
483
+
484
+ return align, row_length
485
+ @classmethod
486
+ def create_from_image(cls,
487
+ image_data: ImageData | ImageDataRegion,
488
+ tex_type: TextureType = TextureType.TYPE_2D,
489
+ internal_format_size: int = 8,
490
+ filters: TextureFilter | tuple[TextureFilter, TextureFilter] | None = None,
491
+ address_mode: AddressMode = AddressMode.REPEAT,
492
+ anisotropic_level: int = 0,
493
+ context: OpenGLSurfaceContext | None = None,
494
+ ) -> Texture:
495
+ """Create a Texture from image data.
496
+
497
+ Args:
498
+ image_data:
499
+ The image instance.
500
+ tex_type:
501
+ The texture enum type.
502
+ internal_format_size:
503
+ Byte size of the internal format.
504
+ filters:
505
+ Texture format filter, passed as a list of min/mag filters, or a single filter to apply both.
506
+ address_mode:
507
+ Texture address mode.
508
+ anisotropic_level:
509
+ The maximum anisotropic level.
510
+ context:
511
+ A specific OpenGL Surface context, otherwise the current active context.
512
+
513
+ Returns:
514
+ A currently bound texture.
515
+ """
516
+ ctx = context or pyglet.graphics.api.core.current_context
517
+ gl = pyglet.graphics.api.core.current_context.gl
518
+
519
+ tex_id = gl.createTexture()
520
+ target = texture_map[tex_type]
521
+ gl.bindTexture(target, tex_id)
522
+
523
+ texture = cls(ctx, image_data.width, image_data.height, tex_id, tex_type,
524
+ ComponentFormat(image_data.format), internal_format_size, image_data.data_type, filters,
525
+ address_mode, anisotropic_level)
526
+
527
+ gl.texParameteri(target, GL_TEXTURE_MIN_FILTER, texture._gl_min_filter)
528
+ gl.texParameteri(target, GL_TEXTURE_MAG_FILTER, texture._gl_mag_filter)
529
+
530
+ pixel_fmt = image_data.format
531
+ image_bytes = image_data.get_bytes(pixel_fmt, image_data.width * len(pixel_fmt))
532
+ buffer = pyodide.ffi.to_js(memoryview(image_bytes))
533
+ js_array = js.Uint8Array.new(buffer)
534
+ gl_pfmt, gl_type = _get_pixel_format(image_data)
535
+
536
+ align, row_length = texture._get_image_alignment(image_data)
537
+
538
+ gl.pixelStorei(GL_PACK_ALIGNMENT, align)
539
+ gl.pixelStorei(GL_UNPACK_ROW_LENGTH, row_length)
540
+
541
+ gl.texImage2D(
542
+ target,
543
+ 0,
544
+ texture._gl_internal_format,
545
+ image_data.width,
546
+ image_data.height,
547
+ 0,
548
+ gl_pfmt,
549
+ gl_type,
550
+ js_array,
551
+ )
552
+ gl.flush()
553
+ return texture
554
+
555
+ @classmethod
556
+ def create(cls, width: int, height: int,
557
+ tex_type: TextureType = TextureType.TYPE_2D,
558
+ internal_format: ComponentFormat = ComponentFormat.RGBA,
559
+ internal_format_size: int = 8,
560
+ internal_format_type: str = "B",
561
+ filters: TextureFilter | tuple[TextureFilter, TextureFilter] | None = None,
562
+ address_mode: AddressMode = AddressMode.REPEAT,
563
+ anisotropic_level: int = 0,
564
+ blank_data: bool = True, context: OpenGLSurfaceContext | None = None) -> TextureBase:
565
+ """Create a Texture.
566
+
567
+ Create a Texture with the specified dimensions, and attributes.
568
+
569
+ Args:
570
+ width:
571
+ Width of texture in pixels.
572
+ height:
573
+ Height of texture in pixels.
574
+ tex_type:
575
+ The texture enum type.
576
+ internal_format:
577
+ Component format of the image data.
578
+ internal_format_size:
579
+ Byte size of the internal format.
580
+ internal_format_type:
581
+ Internal format type in struct format.
582
+ filters:
583
+ Texture format filter, passed as a list of min/mag filter or a single filter to apply both.
584
+ address_mode:
585
+ Texture address mode.
586
+ anisotropic_level:
587
+ The maximum anisotropic level.
588
+ blank_data:
589
+ If True, initialize the texture data with all zeros. If False, do not pass initial data.
590
+ context:
591
+ A specific OpenGL Surface context, otherwise the current active context.
592
+
593
+ Returns:
594
+ A currently bound texture.
595
+ """
596
+ ctx = context or pyglet.graphics.api.core.current_context
597
+ gl = ctx.gl
598
+
599
+ tex_id = gl.createTexture()
600
+ target = texture_map[tex_type]
601
+
602
+ texture = cls(ctx, width, height, tex_id, tex_type, internal_format, internal_format_size, internal_format_type, filters, address_mode, anisotropic_level)
603
+ gl.bindTexture(target, tex_id)
604
+
605
+ gl.texParameteri(target, GL_TEXTURE_MIN_FILTER, texture._gl_min_filter)
606
+ gl.texParameteri(target, GL_TEXTURE_MAG_FILTER, texture._gl_mag_filter)
607
+
608
+ data = js.Uint8Array.new(width * height * len(internal_format)) if blank_data else None
609
+ texture._allocate(data)
610
+ return texture
611
+
612
+ def _allocate(self, data: None | js.Uint8Array) -> None:
613
+ self._gl.texImage2D(self.target, 0,
614
+ self._gl_internal_format,
615
+ self.width, self.height,
616
+ 0,
617
+ _get_base_format(self.internal_format),
618
+ GL_UNSIGNED_BYTE,
619
+ data)
620
+ self._gl.flush()
621
+
622
+ def _attach_texture_to_fbo(self, z: int = 0) -> None:
623
+ self._gl.framebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self.id, self.level)
624
+
625
+ def fetch(self, z: int = 0) -> ImageData:
626
+ """Fetch the image data of this texture from the GPU.
627
+
628
+ Bind the texture, and read the pixel data back from the GPU.
629
+ This can be a somewhat costly operation.
630
+ Modifying the returned ImageData object has no effect on the
631
+ texture itself. Uploading ImageData back to the GPU/texture
632
+ can be done with the :py:meth:`~Texture.upload` method.
633
+
634
+ Args:
635
+ z:
636
+ For 3D textures, the image slice to retrieve.
637
+ """
638
+ self._gl.bindTexture(self.target, self.id)
639
+
640
+ # Always extract complete RGBA data. Could check internalformat
641
+ # to only extract used channels. XXX
642
+ fmt = 'RGBA'
643
+ gl_format = GL_RGBA
644
+
645
+ buffer_size = self.width * self.height * self.images * len(fmt)
646
+
647
+ self._gl.pixelStorei(GL_PACK_ALIGNMENT, 1)
648
+ fbo = self._gl.createFramebuffer()
649
+ self._gl.bindFramebuffer(GL_FRAMEBUFFER, fbo)
650
+ self._attach_texture_to_fbo(z)
651
+
652
+ if self._gl.checkFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE:
653
+ raise Exception("Framebuffer is incomplete.")
654
+
655
+ pixel_buf = js.Uint8Array(buffer_size)
656
+ self._gl.readPixels(0, 0, self.width, self.height, gl_format, GL_UNSIGNED_BYTE, pixel_buf)
657
+ self._gl.bindFramebuffer(GL_FRAMEBUFFER, None)
658
+ self._gl.deleteFramebuffer(fbo)
659
+
660
+ data = ImageData(self.width, self.height, fmt, pixel_buf)
661
+ return data
662
+
663
+ def get_image_data(self, z: int = 0) -> ImageData:
664
+ """Get the image data of this texture.
665
+
666
+ Bind the texture, and read the pixel data back from the GPU.
667
+ This can be a somewhat costly operation.
668
+ Modifying the returned ImageData object has no effect on the
669
+ texture itself. Uploading ImageData back to the GPU/texture
670
+ can be done with the :py:meth:`~Texture.upload` method.
671
+
672
+ Args:
673
+ z:
674
+ For 3D textures, the image slice to retrieve.
675
+ """
676
+ return self.fetch(z)
677
+
678
+ def _apply_region_unpack(self, image_data: ImageData | ImageDataRegion) -> None:
679
+ if isinstance(image_data, ImageDataRegion):
680
+ self._gl.pixelStorei(GL_UNPACK_SKIP_PIXELS, image_data.x)
681
+ self._gl.pixelStorei(GL_UNPACK_SKIP_ROWS, image_data.y)
682
+
683
+ def _default_region_unpack(self, image_data: ImageData | ImageDataRegion) -> None:
684
+ if isinstance(image_data, ImageDataRegion):
685
+ self._gl.pixelStorei(GL_UNPACK_SKIP_PIXELS, 0)
686
+ self._gl.pixelStorei(GL_UNPACK_SKIP_ROWS, 0)
687
+
688
+ def upload(self, image_data: ImageData | ImageDataRegion, x: int, y: int, z: int) -> None:
689
+ """Upload image data into the Texture at specific coordinates.
690
+
691
+ You must have this texture bound before uploading data.
692
+
693
+ The image's anchor point will be aligned to the given ``x`` and ``y``
694
+ coordinates. If this texture is a 3D texture, the ``z``
695
+ parameter gives the image slice to blit into.
696
+ """
697
+ x -= self.anchor_x
698
+ y -= self.anchor_y
699
+
700
+ data_format = image_data.format
701
+ data_pitch = abs(image_data._current_pitch)
702
+
703
+ fmt, gl_type = _get_pixel_format(image_data)
704
+
705
+ # Get data in required format (hopefully will be the same format it's already
706
+ # in, unless that's an obscure format, upside-down or the driver is old).
707
+ data = image_data.convert(data_format, data_pitch)
708
+
709
+ js_array = js.Uint8Array.new(data)
710
+
711
+ if data_pitch & 0x1:
712
+ align = 1
713
+ elif data_pitch & 0x2:
714
+ align = 2
715
+ else:
716
+ align = 4
717
+ row_length = data_pitch // len(data_format)
718
+
719
+ gl = self._gl
720
+ gl.pixelStorei(GL_UNPACK_ALIGNMENT, align)
721
+ gl.pixelStorei(GL_UNPACK_ROW_LENGTH, row_length)
722
+ self._apply_region_unpack(image_data)
723
+
724
+ if self.target == GL_TEXTURE_3D or self.target == GL_TEXTURE_2D_ARRAY:
725
+ gl.texSubImage3D(
726
+ self.target, self.level, x, y, z, image_data.width, image_data.height, 1, fmt, gl_type, js_array,
727
+ )
728
+ else:
729
+ gl.texSubImage2D(self.target, self.level, x, y, image_data.width, image_data.height, fmt, gl_type, js_array)
730
+
731
+ gl.pixelStorei(GL_UNPACK_ROW_LENGTH, 0)
732
+ self._default_region_unpack(image_data)
733
+
734
+ # Flush image upload before data gets GC'd:
735
+ gl.flush()
736
+
737
+ def get_texture(self) -> TextureBase:
738
+ return self
739
+
740
+ def get_mipmapped_texture(self) -> TextureBase:
741
+ raise NotImplementedError(f"Not implemented for {self}.")
742
+
743
+ def get_region(self, x: int, y: int, width: int, height: int) -> TextureRegionBase:
744
+ return self.region_class(x, y, 0, width, height, self)
745
+
746
+ def get_transform(
747
+ self, flip_x: bool = False, flip_y: bool = False, rotate: Literal[0, 90, 180, 270, 360] = 0,
748
+ ) -> TextureRegionBase:
749
+ """Create a copy of this image applying a simple transformation.
750
+
751
+ The transformation is applied to the texture coordinates only;
752
+ :py:meth:`~pyglet.image.AbstractImage.get_image_data` will return the
753
+ untransformed data. The transformation is applied around the anchor point.
754
+
755
+ Args:
756
+ flip_x:
757
+ If True, the returned image will be flipped horizontally.
758
+ flip_y:
759
+ If True, the returned image will be flipped vertically.
760
+ rotate:
761
+ Degrees of clockwise rotation of the returned image. Only
762
+ 90-degree increments are supported.
763
+ """
764
+ transform = self.get_region(0, 0, self.width, self.height)
765
+ bl, br, tr, tl = 0, 1, 2, 3
766
+ transform.anchor_x = self.anchor_x
767
+ transform.anchor_y = self.anchor_y
768
+ if flip_x:
769
+ bl, br, tl, tr = br, bl, tr, tl
770
+ transform.anchor_x = self.width - self.anchor_x
771
+ if flip_y:
772
+ bl, br, tl, tr = tl, tr, bl, br
773
+ transform.anchor_y = self.height - self.anchor_y
774
+ rotate %= 360
775
+ if rotate < 0:
776
+ rotate += 360
777
+ if rotate == 0:
778
+ pass
779
+ elif rotate == 90:
780
+ bl, br, tr, tl = br, tr, tl, bl
781
+ transform.anchor_x, transform.anchor_y = transform.anchor_y, transform.width - transform.anchor_x
782
+ elif rotate == 180:
783
+ bl, br, tr, tl = tr, tl, bl, br
784
+ transform.anchor_x = transform.width - transform.anchor_x
785
+ transform.anchor_y = transform.height - transform.anchor_y
786
+ elif rotate == 270:
787
+ bl, br, tr, tl = tl, bl, br, tr
788
+ transform.anchor_x, transform.anchor_y = transform.height - transform.anchor_y, transform.anchor_x
789
+ else:
790
+ raise ImageException("Only 90 degree rotations are supported.")
791
+ if rotate in (90, 270):
792
+ transform.width, transform.height = transform.height, transform.width
793
+ transform._set_tex_coords_order(bl, br, tr, tl)
794
+ return transform
795
+
796
+ def _set_tex_coords_order(self, bl, br, tr, tl) -> None:
797
+ tex_coords = (self.tex_coords[:3], self.tex_coords[3:6], self.tex_coords[6:9], self.tex_coords[9:])
798
+ self.tex_coords = tex_coords[bl] + tex_coords[br] + tex_coords[tr] + tex_coords[tl]
799
+
800
+ order = self.tex_coords_order
801
+ self.tex_coords_order = (order[bl], order[br], order[tr], order[tl])
802
+
803
+ @property
804
+ def uv(self) -> tuple[float, float, float, float]:
805
+ """Tuple containing the left, bottom, right, top 2D texture coordinates."""
806
+ tex_coords = self.tex_coords
807
+ return tex_coords[0], tex_coords[1], tex_coords[3], tex_coords[7]
808
+
809
+ def __repr__(self) -> str:
810
+ return f"{self.__class__.__name__}(id={self.id}, size={self.width}x{self.height})"
811
+
812
+
813
+ class TextureRegion(Texture):
814
+ """A rectangular region of a texture, presented as if it were a separate texture."""
815
+
816
+ def __init__(self, x: int, y: int, z: int, width: int, height: int, owner: TextureBase):
817
+ super().__init__(owner._context, width, height, owner.id, owner.tex_type, owner.internal_format,
818
+ owner.internal_format_size, owner.internal_format_type, owner.filters, owner.address_mode,
819
+ owner.anisotropic_level)
820
+
821
+ self.x = x
822
+ self.y = y
823
+ self.z = z
824
+ self._width = width
825
+ self._height = height
826
+ self.owner = owner
827
+ owner_u1 = owner.tex_coords[0]
828
+ owner_v1 = owner.tex_coords[1]
829
+ owner_u2 = owner.tex_coords[3]
830
+ owner_v2 = owner.tex_coords[7]
831
+ scale_u = owner_u2 - owner_u1
832
+ scale_v = owner_v2 - owner_v1
833
+ u1 = x / owner.width * scale_u + owner_u1
834
+ v1 = y / owner.height * scale_v + owner_v1
835
+ u2 = (x + width) / owner.width * scale_u + owner_u1
836
+ v2 = (y + height) / owner.height * scale_v + owner_v1
837
+ r = z / owner.images + owner.tex_coords[2]
838
+ self.tex_coords = (u1, v1, r, u2, v1, r, u2, v2, r, u1, v2, r)
839
+
840
+ def fetch(self, _z: int = 0) -> ImageDataRegion:
841
+ image_data = self.owner.fetch(self.z)
842
+ return image_data.get_region(self.x, self.y, self.width, self.height)
843
+
844
+ def get_image_data(self):
845
+ return self.fetch()
846
+
847
+ def get_region(self, x: int, y: int, width: int, height: int) -> TextureRegionBase:
848
+ x += self.x
849
+ y += self.y
850
+ region = self.region_class(x, y, self.z, width, height, self.owner)
851
+ region._set_tex_coords_order(*self.tex_coords_order)
852
+ return region
853
+
854
+ def upload(self, source: _AbstractImage, x: int, y: int, z: int) -> None:
855
+ assert source.width <= self._width and source.height <= self._height, f"{source} is larger than {self}"
856
+ self.owner.blit_into(source, x + self.x, y + self.y, z + self.z)
857
+
858
+ def __repr__(self) -> str:
859
+ return (
860
+ f"{self.__class__.__name__}(id={self.id},"
861
+ f" size={self.width}x{self.height}, owner={self.owner.width}x{self.owner.height})"
862
+ )
863
+
864
+ def delete(self) -> None:
865
+ """Deleting a TextureRegion has no effect. Operate on the owning texture instead."""
866
+
867
+ def __del__(self):
868
+ pass
869
+
870
+
871
+ Texture.region_class = TextureRegion
872
+
873
+
874
+ class Texture3D(Texture, UniformTextureSequence):
875
+ """A texture with more than one image slice.
876
+
877
+ Use the :py:meth:`create_for_images` or :py:meth:`create_for_image_grid`
878
+ classmethod to construct a Texture3D.
879
+ """
880
+
881
+ item_width: int = 0
882
+ item_height: int = 0
883
+ items: tuple
884
+
885
+ @classmethod
886
+ def create_for_images(cls, images,
887
+ internal_format_size: int = 8,
888
+ internal_format_type: str = "b",
889
+ filters: TextureFilter | tuple[TextureFilter, TextureFilter] | None = None,
890
+ address_mode: AddressMode = AddressMode.REPEAT,
891
+ anisotropic_level: int = 0,
892
+ context: OpenGLSurfaceContext | None = None) -> Texture3D:
893
+ ctx = context or pyglet.graphics.api.core.current_context
894
+ gl = ctx.gl
895
+ item_width = images[0].width
896
+ item_height = images[0].height
897
+ pixel_fmt = images[0].format
898
+ internal_format = ComponentFormat(pixel_fmt)
899
+
900
+ if not all(img.width == item_width and img.height == item_height for img in images):
901
+ raise ImageException('Images do not have same dimensions.')
902
+
903
+ tex_id = gl.createTexture()
904
+ target = texture_map[TextureType.TYPE_3D]
905
+ gl.bindTexture(target, tex_id)
906
+ texture = cls(ctx, item_width, item_height, tex_id, TextureType.TYPE_3D, internal_format, internal_format_size,
907
+ internal_format_type, filters, address_mode, anisotropic_level)
908
+ gl.texParameteri(target, GL_TEXTURE_MIN_FILTER, texture._gl_min_filter)
909
+ gl.texParameteri(target, GL_TEXTURE_MAG_FILTER, texture._gl_mag_filter)
910
+
911
+ base_image = images[0]
912
+ if base_image.anchor_x or base_image.anchor_y:
913
+ texture.anchor_x = base_image.anchor_x
914
+ texture.anchor_y = base_image.anchor_y
915
+
916
+ texture.images = len(images)
917
+
918
+ size = (texture.width * texture.height * texture.images * len(internal_format))
919
+ data = js.Uint8Array.new(size)
920
+ texture._allocate(data)
921
+
922
+ items = []
923
+ for i, image in enumerate(images):
924
+ item = cls.region_class(0, 0, i, item_width, item_height, texture)
925
+ items.append(item)
926
+ texture.upload(image, image.anchor_x, image.anchor_y, z=i)
927
+ gl.flush()
928
+
929
+ texture.items = items
930
+ texture.item_width = item_width
931
+ texture.item_height = item_height
932
+ return texture
933
+
934
+ def _allocate(self, data: None | js.Uint8Array) -> None:
935
+ self._gl.texImage3D(self.target, 0,
936
+ self._gl_internal_format,
937
+ self.width, self.height, self.images,
938
+ 0,
939
+ _get_base_format(self.internal_format),
940
+ GL_UNSIGNED_BYTE,
941
+ 0)
942
+
943
+ def _attach_texture_to_fbo(self, z: int = 0) -> None:
944
+ self._gl.framebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, self.id, self.level, z)
945
+
946
+ def __len__(self):
947
+ return len(self.items)
948
+
949
+ def __getitem__(self, index):
950
+ return self.items[index]
951
+
952
+ def __setitem__(self, index, value):
953
+ if type(index) is slice:
954
+ self._gl.bindTexture(self.target, self.id)
955
+
956
+ for item, image in zip(self[index], value):
957
+ self.upload(self, image, image.anchor_x, image.anchor_y, item.z)
958
+ else:
959
+ self.upload(value, value.anchor_x, value.anchor_y, self[index].z)
960
+
961
+ def __iter__(self) -> Iterator[TextureRegionBase]:
962
+ return iter(self.items)
963
+
964
+
965
+ class TextureArrayRegion(TextureRegion):
966
+ """A region of a TextureArray, presented as if it were a separate texture."""
967
+
968
+ def __repr__(self):
969
+ return f"{self.__class__.__name__}(id={self.id}, size={self.width}x{self.height}, layer={self.z})"
970
+
971
+
972
+ class TextureArray(Texture, UniformTextureSequence):
973
+ items: list[TextureArrayRegion]
974
+
975
+ def __init__(self, context: OpenGLSurfaceContext, width, height, tex_id, max_depth,
976
+ internal_format: ComponentFormat = ComponentFormat.RGBA,
977
+ internal_format_size: int = 8,
978
+ internal_format_type: str = "b",
979
+ filters: TextureFilter | tuple[TextureFilter, TextureFilter] | None = None,
980
+ address_mode: AddressMode = AddressMode.REPEAT,
981
+ anisotropic_level: int = 0):
982
+ super().__init__(context, width, height, tex_id, TextureType.TYPE_2D_ARRAY, internal_format, internal_format_size,
983
+ internal_format_type, filters, address_mode, anisotropic_level)
984
+ self.max_depth = max_depth
985
+ self.items = []
986
+
987
+ @classmethod
988
+ def create(cls, width: int, height: int,
989
+ max_depth: int = 256,
990
+ internal_format: ComponentFormat = ComponentFormat.RGBA,
991
+ internal_format_size: int = 8,
992
+ internal_format_type: str = "b",
993
+ filters: TextureFilter | tuple[TextureFilter, TextureFilter] | None = None,
994
+ address_mode: AddressMode = AddressMode.REPEAT,
995
+ anisotropic_level: int = 0,
996
+ context: OpenGLSurfaceContext | None = None) -> TextureArray:
997
+ """Create an empty TextureArray.
998
+
999
+ You may specify the maximum depth, or layers, the Texture Array should have. This defaults
1000
+ to 256, but will be hardware and driver dependent.
1001
+
1002
+ Args:
1003
+ width:
1004
+ Width of the texture.
1005
+ height:
1006
+ Height of the texture.
1007
+ descriptor:
1008
+ Texture description.
1009
+ max_depth:
1010
+ The number of layers in the texture array.
1011
+
1012
+ .. versionadded:: 2.0
1013
+ """
1014
+ ctx = pyglet.graphics.api.core.current_context or context
1015
+
1016
+ max_depth_limit = get_max_array_texture_layers()
1017
+ assert max_depth <= max_depth_limit, f"TextureArray max_depth supported is {max_depth_limit}."
1018
+
1019
+ gl = ctx.gl
1020
+
1021
+ tex_id = gl.createTexture()
1022
+
1023
+
1024
+ texture = cls(ctx, width, height, tex_id, max_depth, internal_format, internal_format_size,
1025
+ internal_format_type, filters, address_mode, anisotropic_level)
1026
+
1027
+ gl.bindTexture(texture.target, tex_id)
1028
+ gl.texParameteri(texture.target, GL_TEXTURE_MIN_FILTER, texture._gl_min_filter)
1029
+ gl.texParameteri(texture.target, GL_TEXTURE_MAG_FILTER, texture._gl_mag_filter)
1030
+
1031
+ texture._allocate(None)
1032
+ return texture
1033
+
1034
+ def _update_subregion(self, image_data: ImageData, x: int, y: int, z: int):
1035
+ data_pitch = abs(image_data._current_pitch)
1036
+
1037
+ data = image_data.convert(image_data.format, data_pitch)
1038
+
1039
+ fmt, gl_type = _get_pixel_format(image_data)
1040
+
1041
+ self._gl.texSubImage3D(self.target, self.level,
1042
+ x, y, z,
1043
+ image_data.width, image_data.height, 1,
1044
+ fmt, gl_type,
1045
+ data)
1046
+
1047
+ def _attach_texture_to_fbo(self, z: int = 0) -> None:
1048
+ self._gl.framebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, self.id, self.level, z)
1049
+
1050
+ def _allocate(self, data: None | js.Uint8Array) -> None:
1051
+ self._gl.texImage3D(self.target, 0,
1052
+ self._gl_internal_format,
1053
+ self.width, self.height, self.max_depth,
1054
+ 0,
1055
+ _get_base_format(self.internal_format),
1056
+ GL_UNSIGNED_BYTE,
1057
+ 0)
1058
+
1059
+ def _verify_size(self, image: _AbstractImage) -> None:
1060
+ if image.width > self.width or image.height > self.height:
1061
+ raise TextureArraySizeExceeded(
1062
+ f'Image ({image.width}x{image.height}) exceeds the size of the TextureArray ({self.width}x'
1063
+ f'{self.height})',
1064
+ )
1065
+
1066
+ def add(self, image: pyglet.image.ImageData) -> TextureArrayRegion:
1067
+ if len(self.items) >= self.max_depth:
1068
+ raise TextureArrayDepthExceeded("TextureArray is full.")
1069
+
1070
+ self._verify_size(image)
1071
+ start_length = len(self.items)
1072
+ item = self.region_class(0, 0, start_length, image.width, image.height, self)
1073
+
1074
+ self.upload(image, image.anchor_x, image.anchor_y, start_length)
1075
+ self.items.append(item)
1076
+ return item
1077
+
1078
+ def allocate(self, *images: _AbstractImage) -> list[TextureArrayRegion]:
1079
+ """Allocates multiple images at once."""
1080
+ if len(self.items) + len(images) > self.max_depth:
1081
+ raise TextureArrayDepthExceeded("The amount of images being added exceeds the depth of this TextureArray.")
1082
+
1083
+ self._gl.bindTexture(self.target, self.id)
1084
+
1085
+ start_length = len(self.items)
1086
+ for i, image in enumerate(images):
1087
+ self._verify_size(image)
1088
+ item = self.region_class(0, 0, start_length + i, image.width, image.height, self)
1089
+ self.items.append(item)
1090
+ image.blit_to_texture(self.target, self.level, image.anchor_x, image.anchor_y, start_length + i)
1091
+
1092
+ return self.items[start_length:]
1093
+
1094
+ @classmethod
1095
+ def create_for_image_grid(cls, grid) -> TextureArray:
1096
+ texture_array = cls.create(grid[0].width, grid[0].height,
1097
+ internal_format=ComponentFormat(grid[0].format),
1098
+ max_depth=len(grid))
1099
+ texture_array.allocate(*grid[:])
1100
+ return texture_array
1101
+
1102
+ def __len__(self) -> int:
1103
+ return len(self.items)
1104
+
1105
+ def __getitem__(self, index) -> TextureArrayRegion:
1106
+ return self.items[index]
1107
+
1108
+ def __setitem__(self, index, value) -> None:
1109
+ if type(index) is slice:
1110
+ self._gl.bindTexture(self.target, self.id)
1111
+
1112
+ for old_item, image in zip(self[index], value):
1113
+ self._verify_size(image)
1114
+ item = self.region_class(0, 0, old_item.z, image.width, image.height, self)
1115
+ image.blit_to_texture(self.target, self.level, image.anchor_x, image.anchor_y, old_item.z)
1116
+ self.items[old_item.z] = item
1117
+ else:
1118
+ self._verify_size(value)
1119
+ item = self.region_class(0, 0, index, value.width, value.height, self)
1120
+ self.upload(value, value.anchor_x, value.anchor_y, index)
1121
+ self.items[index] = item
1122
+
1123
+ def __iter__(self) -> Iterator[TextureRegionBase]:
1124
+ return iter(self.items)
1125
+
1126
+
1127
+ TextureArray.region_class = TextureArrayRegion
1128
+ TextureArrayRegion.region_class = TextureArrayRegion
1129
+
1130
+
1131
+ class TextureGrid(_AbstractGrid[Union[Texture, TextureRegion]]):
1132
+ """A texture containing a regular grid of texture regions.
1133
+
1134
+ To construct, create an :py:class:`~pyglet.image.ImageGrid` first::
1135
+
1136
+ image_grid = ImageGrid(...)
1137
+ texture_grid = TextureGrid(image_grid)
1138
+
1139
+ The texture grid can be accessed as a single texture, or as a sequence
1140
+ of :py:class:`~pyglet.graphics.TextureRegion`. When accessing as a sequence, you can specify
1141
+ integer indexes, in which the images are arranged in rows from the
1142
+ bottom-left to the top-right::
1143
+
1144
+ # assume the texture_grid is 3x3:
1145
+ current_texture = texture_grid[3] # get the middle-left image
1146
+
1147
+ You can also specify tuples in the sequence methods, which are addressed
1148
+ as ``row, column``::
1149
+
1150
+ # equivalent to the previous example:
1151
+ current_texture = texture_grid[1, 0]
1152
+
1153
+ When using tuples in a slice, the returned sequence is over the
1154
+ rectangular region defined by the slice::
1155
+
1156
+ # returns center, center-right, center-top, top-right images in that
1157
+ # order:
1158
+ images = texture_grid[(1,1):]
1159
+ # equivalent to
1160
+ images = texture_grid[(1,1):(3,3)]
1161
+
1162
+ """
1163
+
1164
+ def __init__(
1165
+ self,
1166
+ texture: Texture | TextureRegion,
1167
+ rows: int,
1168
+ columns: int,
1169
+ item_width: int,
1170
+ item_height: int,
1171
+ row_padding: int = 0,
1172
+ column_padding: int = 0,
1173
+ ) -> None:
1174
+ """Construct a grid for the given image.
1175
+
1176
+ You can specify parameters for the grid, for example setting
1177
+ the padding between cells. Grids are always aligned to the
1178
+ bottom-left corner of the image.
1179
+
1180
+ Args:
1181
+ texture:
1182
+ A texture or region over which to construct the grid.
1183
+ rows:
1184
+ Number of rows in the grid.
1185
+ columns:
1186
+ Number of columns in the grid.
1187
+ item_width:
1188
+ Width of each column. If unspecified, is calculated such
1189
+ that the entire texture width is used.
1190
+ item_height:
1191
+ Height of each row. If unspecified, is calculated such that
1192
+ the entire texture height is used.
1193
+ row_padding:
1194
+ Pixels separating adjacent rows. The padding is only
1195
+ inserted between rows, not at the edges of the grid.
1196
+ column_padding:
1197
+ Pixels separating adjacent columns. The padding is only
1198
+ inserted between columns, not at the edges of the grid.
1199
+ """
1200
+ if isinstance(texture, TextureRegion):
1201
+ owner = texture.owner
1202
+ else:
1203
+ owner = texture
1204
+
1205
+ item_width = item_width or (texture.width - column_padding * (columns - 1)) // columns
1206
+ item_height = item_height or (texture.height - row_padding * (rows - 1)) // rows
1207
+ self.texture = owner
1208
+ super().__init__(rows, columns, item_width, item_height, row_padding, column_padding)
1209
+
1210
+ @classmethod
1211
+ def from_image_grid(cls, image_grid: ImageGrid) -> TextureGrid:
1212
+ texture = image_grid.image.get_texture()
1213
+ return cls(
1214
+ texture,
1215
+ image_grid.rows,
1216
+ image_grid.columns,
1217
+ image_grid.item_width,
1218
+ image_grid.item_height,
1219
+ image_grid.row_padding,
1220
+ image_grid.column_padding,
1221
+ )
1222
+
1223
+ def _create_item(self, x: int, y: int, width: int, height: int) -> TextureRegion:
1224
+ return self.texture.get_region(x, y, width, height)
1225
+
1226
+ def _update_item(self, existing_item: T, new_item: T) -> None:
1227
+ existing_item.upload(new_item, new_item.anchor_x, new_item.anchor_y, 0)