pyglet 2.1.dev5__py3-none-any.whl → 2.1.dev6__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 (47) hide show
  1. pyglet/__init__.py +33 -14
  2. pyglet/__init__.pyi +15 -1
  3. pyglet/app/__init__.py +1 -1
  4. pyglet/app/base.py +1 -1
  5. pyglet/app/cocoa.py +4 -0
  6. pyglet/display/__init__.py +5 -1
  7. pyglet/display/__init__.pyi +4 -0
  8. pyglet/event.py +11 -15
  9. pyglet/experimental/geoshader_sprite.py +12 -9
  10. pyglet/experimental/jobs.py +124 -0
  11. pyglet/experimental/multitexture_sprite.py +20 -11
  12. pyglet/experimental/particles.py +11 -9
  13. pyglet/gl/__init__.py +5 -3
  14. pyglet/graphics/__init__.py +57 -19
  15. pyglet/graphics/shader.py +18 -15
  16. pyglet/graphics/vertexbuffer.py +80 -36
  17. pyglet/graphics/vertexdomain.py +99 -144
  18. pyglet/gui/widgets.py +31 -18
  19. pyglet/image/__init__.py +570 -611
  20. pyglet/image/animation.py +7 -8
  21. pyglet/image/codecs/png.py +2 -2
  22. pyglet/input/__init__.py +6 -6
  23. pyglet/input/base.py +2 -0
  24. pyglet/input/linux/evdev.py +29 -39
  25. pyglet/math.py +103 -72
  26. pyglet/media/codecs/base.py +3 -109
  27. pyglet/media/drivers/base.py +88 -4
  28. pyglet/media/player.py +2 -9
  29. pyglet/model/__init__.py +34 -41
  30. pyglet/model/codecs/base.py +104 -0
  31. pyglet/model/codecs/gltf.py +295 -172
  32. pyglet/model/codecs/obj.py +33 -39
  33. pyglet/resource.py +6 -9
  34. pyglet/shapes.py +8 -5
  35. pyglet/sprite.py +23 -4
  36. pyglet/text/__init__.py +1 -1
  37. pyglet/window/__init__.py +87 -49
  38. pyglet/window/cocoa/__init__.py +35 -13
  39. pyglet/window/cocoa/pyglet_delegate.py +36 -8
  40. pyglet/window/cocoa/pyglet_view.py +13 -4
  41. pyglet/window/headless/__init__.py +1 -1
  42. pyglet/window/win32/__init__.py +50 -28
  43. pyglet/window/xlib/__init__.py +26 -16
  44. {pyglet-2.1.dev5.dist-info → pyglet-2.1.dev6.dist-info}/METADATA +1 -1
  45. {pyglet-2.1.dev5.dist-info → pyglet-2.1.dev6.dist-info}/RECORD +47 -44
  46. {pyglet-2.1.dev5.dist-info → pyglet-2.1.dev6.dist-info}/LICENSE +0 -0
  47. {pyglet-2.1.dev5.dist-info → pyglet-2.1.dev6.dist-info}/WHEEL +0 -0
pyglet/__init__.py CHANGED
@@ -8,14 +8,14 @@ import os
8
8
  import sys
9
9
  from collections.abc import ItemsView, Sequence
10
10
  from dataclasses import dataclass
11
- from typing import TYPE_CHECKING
11
+ from typing import TYPE_CHECKING, Literal
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from types import FrameType
15
15
  from typing import Any, Callable, ItemsView, Sized
16
16
 
17
17
  #: The release version
18
- version = '2.1.dev5'
18
+ version = '2.1.dev6'
19
19
  __version__ = version
20
20
 
21
21
  MIN_PYTHON_VERSION = 3, 8
@@ -48,6 +48,7 @@ _OPTION_TYPE_VALIDATORS = {
48
48
  "int": lambda x: isinstance(x, int),
49
49
  }
50
50
 
51
+
51
52
  @dataclass
52
53
  class Options:
53
54
  """Dataclass for global pyglet options."""
@@ -249,16 +250,29 @@ class Options:
249
250
 
250
251
  .. versionadded:: 2.0.5"""
251
252
 
252
- scale_with_dpi: bool = False
253
- """For 'HiDPI' ('Retina') displays, scale Window creation size with desktop scaling. Defaults to ``False``.
254
-
255
- For high pixel density displays, it is common for the desktop to have some form of application window scaling.
256
- Setting this option to ``True`` will make pyglet aware of these settings when Windows are created. For instance,
257
- if the desktop scaling factor is set to 150%, a Window created with a resolution of 1000x1000 will have an actual
258
- framebuffer size of 1500x1500. Keep in mind that pyglet objects may not be scaled proportionately, so this is left
259
- up to the developer. The :py:attr:`~pyglet.window.Window.scale` & :py:attr:`~pyglet.window.Window.dpi` attributes
260
- can be queried as a reference when determining object creation. By default, windows are created with the actual
261
- number of pixels requested.
253
+ dpi_scaling: Literal["real", "scaled", "stretch"] = "real"
254
+ """For 'HiDPI' displays, Window behavior can differ between operating systems. Defaults to `'real'`.
255
+
256
+ The current options are an attempt to create consistent behavior across all of the operating systems.
257
+
258
+ `'real'` (default): Provides a 1:1 pixel for Window frame size and framebuffer. Primarily used for game applications
259
+ to ensure you are getting the exact pixels for the resolution. If you provide an 800x600 window, you can ensure it
260
+ will be 800x600 pixels when the user chooses it.
261
+
262
+ `'scaled'`: Window size is scaled based on the DPI ratio. Window size and content (projection) size matches the full
263
+ framebuffer. Primarily used for any applications that wish to become DPI aware. You must rescale and reposition your
264
+ content to take advantage of the larger framebuffer. An 800x600 with a 150% DPI scaling would be changed to
265
+ 1200x900 for both `window.get_size` and `window.get_framebuffer_size()`.
266
+
267
+ Keep in mind that pyglet objects may not be scaled proportionately, so this is left up to the developer.
268
+ The :py:attr:`~pyglet.window.Window.scale` & :py:attr:`~pyglet.window.Window.dpi` attributes can be queried as a
269
+ reference when determining object creation.
270
+
271
+ `'stretch'`: Window is scaled based on the DPI ratio. However, content size matches original requested size of the
272
+ window, and is stretched to fit the full framebuffer. This mimics behavior of having no DPI scaling at all. No
273
+ rescaling and repositioning of content will be necessary, but at the cost of blurry content depending on the extent
274
+ of the stretch. For example, 800x600 at 150% DPI will be 800x600 for `window.get_size()` and 1200x900 for
275
+ `window.get_framebuffer_size()`.
262
276
  """
263
277
 
264
278
  shader_bind_management: bool = True
@@ -282,7 +296,7 @@ class Options:
282
296
 
283
297
  def __setitem__(self, key: str, value: Any) -> None:
284
298
  assert key in self.__annotations__, f"Invalid option name: '{key}'"
285
- assert (_SPECIAL_OPTION_VALIDATORS.get(key, None) or _OPTION_TYPE_VALIDATORS[self.__annotations__[key]])(value), \
299
+ assert (_SPECIAL_OPTION_VALIDATORS.get(key) or _OPTION_TYPE_VALIDATORS[self.__annotations__[key]])(value), \
286
300
  f"Invalid type: '{type(value)}' for '{key}'"
287
301
  self.__dict__[key] = value
288
302
 
@@ -290,11 +304,16 @@ class Options:
290
304
  #: Instance of :py:class:`~pyglet.Options` used to set runtime options.
291
305
  options: Options = Options()
292
306
 
307
+ _OPTION_TYPE_REMAPS = {
308
+ "audio": "sequence",
309
+ "vsync": "bool",
310
+ }
293
311
 
294
312
  for _key, _type in options.__annotations__.items():
295
313
  """Check Environment Variables for pyglet options"""
296
314
  if _value := os.environ.get(f"PYGLET_{_key.upper()}"):
297
- if _type == 'tuple':
315
+ _type = _OPTION_TYPE_REMAPS.get(_key, _type)
316
+ if _type == 'sequence':
298
317
  options[_key] = _value.split(",")
299
318
  elif _type == 'bool':
300
319
  options[_key] = _value in ("true", "TRUE", "True", "1")
pyglet/__init__.pyi CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  from dataclasses import dataclass
3
- from typing import Sequence
3
+ from typing import Any, ItemsView, Literal, Sequence
4
4
 
5
5
  from . import app as app
6
6
  from . import clock as clock
@@ -62,6 +62,20 @@ class Options:
62
62
  win32_disable_xinput: bool
63
63
  com_mta: bool
64
64
  osx_alt_loop: bool
65
+ dpi_scaling: Literal["real", "scaled", "stretch"]
66
+ scale_with_dpi: bool
65
67
  shader_bind_management: bool
66
68
 
69
+ def get(self, item: str, default: Any = None) -> Any:
70
+ ...
71
+
72
+ def items(self) -> ItemsView[str, Any]:
73
+ ...
74
+
75
+ def __getitem__(self, item: str) -> Any:
76
+ ...
77
+
78
+ def __setitem__(self, key: str, item: Any) -> None:
79
+ ...
80
+
67
81
  options: Options
pyglet/app/__init__.py CHANGED
@@ -68,7 +68,7 @@ the set when they are no longer referenced or are closed explicitly.
68
68
  """
69
69
 
70
70
 
71
- def run(interval: None | float = 1/60) -> None:
71
+ def run(interval: float | None = 1/60) -> None:
72
72
  """Begin processing events, scheduled functions and window updates.
73
73
 
74
74
  This is a convenience function, equivalent to::
pyglet/app/base.py CHANGED
@@ -112,7 +112,7 @@ class EventLoop(event.EventDispatcher):
112
112
  for window in app.windows:
113
113
  window.draw(dt)
114
114
 
115
- def run(self, interval: None | float = 1/60) -> None:
115
+ def run(self, interval: float | None = 1/60) -> None:
116
116
  """Begin processing events, scheduled functions and window updates.
117
117
 
118
118
  This method enters into the main event loop and, if the ``interval``
pyglet/app/cocoa.py CHANGED
@@ -71,6 +71,10 @@ class _AppDelegate_Implementation:
71
71
  def applicationDidFinishLaunching_(self, notification):
72
72
  self._pyglet_loop._finished_launching = True
73
73
 
74
+ @_AppDelegate.method('B')
75
+ def applicationSupportsSecureRestorableState_(self):
76
+ return True
77
+
74
78
  _AppDelegate = ObjCClass('_AppDelegate') # the actual class
75
79
 
76
80
  class CocoaAlternateEventLoop(EventLoop):
@@ -44,10 +44,13 @@ else:
44
44
  from pyglet.display.win32 import Win32Display as Display
45
45
  from pyglet.display.win32 import Win32Screen as Screen
46
46
  from pyglet.display.win32 import Win32Canvas as Canvas
47
- else:
47
+ elif compat_platform == 'linux':
48
48
  from pyglet.display.xlib import XlibDisplay as Display
49
49
  from pyglet.display.xlib import XlibScreen as Screen
50
50
  from pyglet.display.xlib import XlibCanvas as Canvas
51
+ else:
52
+ msg = f"A display interface for '{compat_platform}' is not yet implemented."
53
+ raise NotImplementedError(msg)
51
54
 
52
55
 
53
56
  _displays: weakref.WeakSet = weakref.WeakSet()
@@ -73,4 +76,5 @@ def get_display() -> Display:
73
76
  # Otherwise, create a new display and return it.
74
77
  return Display()
75
78
 
79
+
76
80
  __all__ = ['Display', 'Screen', 'Canvas', 'ScreenMode', 'get_display']
@@ -0,0 +1,4 @@
1
+ from .base import Display
2
+
3
+ def get_display() -> Display:
4
+ ...
pyglet/event.py CHANGED
@@ -120,23 +120,19 @@ from __future__ import annotations
120
120
 
121
121
  import inspect
122
122
  import os.path
123
-
124
123
  from functools import partial
125
- from typing import TYPE_CHECKING
124
+ from typing import TYPE_CHECKING, Literal, Union
126
125
  from weakref import WeakMethod
127
126
 
128
127
  if TYPE_CHECKING:
129
- import sys
130
128
  from typing import Any, Callable, Generator
131
- if sys.version_info >= (3, 11):
132
- from typing import Self
133
- else:
134
- Self = Any
135
129
 
136
130
 
137
131
  EVENT_HANDLED = True
138
132
  EVENT_UNHANDLED = None
139
133
 
134
+ EVENT_HANDLE_STATE = Union[Literal[True], None]
135
+
140
136
 
141
137
  class EventException(Exception): # noqa: N818
142
138
  """An exception raised when an event handler could not be attached."""
@@ -152,7 +148,7 @@ class EventDispatcher:
152
148
  _event_stack: tuple | list = ()
153
149
 
154
150
  @classmethod
155
- def register_event_type(cls: type[Self], name: str) -> str:
151
+ def register_event_type(cls: type[object], name: str) -> str:
156
152
  """Register an event type with the dispatcher.
157
153
 
158
154
  Before dispatching events, they must first be registered by name.
@@ -161,8 +157,8 @@ class EventDispatcher:
161
157
  for suitable handlers.
162
158
  """
163
159
  if not hasattr(cls, 'event_types'):
164
- cls.event_types = []
165
- cls.event_types.append(name)
160
+ cls.event_types = [] # type: ignore reportAttributeAccessIssue
161
+ cls.event_types.append(name) # type: ignore reportAttributeAccessIssue
166
162
  return name
167
163
 
168
164
  def push_handlers(self, *args: Any, **kwargs: Any) -> None:
@@ -181,15 +177,15 @@ class EventDispatcher:
181
177
  self._event_stack = []
182
178
 
183
179
  # Place dict full of new handlers at beginning of stack
184
- self._event_stack.insert(0, {})
180
+ self._event_stack.insert(0, {}) # type: ignore reportAttributeAccessIssue
185
181
  self.set_handlers(*args, **kwargs)
186
182
 
187
- def _get_handlers(self, args: list, kwargs: dict) -> Generator[str, Callable]:
183
+ def _get_handlers(self, args: list, kwargs: dict) -> Generator[tuple[str, Callable], None, None]:
188
184
  """Implement handler matching on arguments for set_handlers and remove_handlers."""
189
185
  for obj in args:
190
186
  if inspect.isroutine(obj):
191
187
  # Single magically named function
192
- name = obj.__name__
188
+ name: str = obj.__name__
193
189
  if name not in self.event_types:
194
190
  msg = f'Unknown event "{name}"'
195
191
  raise EventException(msg)
@@ -438,10 +434,10 @@ class EventDispatcher:
438
434
  def _dump_handlers(self) -> None:
439
435
 
440
436
  for level, handlers in enumerate(self._event_stack):
441
- print(f"level: {level}")
437
+ print(f"level: {level}")
442
438
 
443
439
  for event_type, handler in handlers.items():
444
- print(f" - '{event_type}': {handler}")
440
+ print(f" - '{event_type}': {handler}")
445
441
 
446
442
  # Decorator
447
443
 
@@ -1,12 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  import sys
2
4
 
3
5
  import pyglet
4
-
6
+ from pyglet import clock, event, graphics, image
5
7
  from pyglet.gl import *
6
- from pyglet import clock
7
- from pyglet import event
8
- from pyglet import graphics
9
- from pyglet import image
10
8
 
11
9
  _is_pyglet_doc_run = hasattr(sys, "is_pyglet_doc_run") and sys.is_pyglet_doc_run
12
10
 
@@ -209,7 +207,7 @@ class SpriteGroup(graphics.Group):
209
207
  self.program.stop()
210
208
 
211
209
  def __repr__(self):
212
- return "{0}({1})".format(self.__class__.__name__, self.texture)
210
+ return f"{self.__class__.__name__}({self.texture})"
213
211
 
214
212
  def __eq__(self, other):
215
213
  return (other.__class__ is self.__class__ and
@@ -328,8 +326,14 @@ class Sprite(event.EventDispatcher):
328
326
  self._group.blend_dest,
329
327
  program,
330
328
  self._user_group)
331
- self._batch.migrate(self._vertex_list, GL_POINTS, self._group, self._batch)
332
- self._program = program
329
+ if (self._batch and
330
+ self._batch.update_shader(self._vertex_list, GL_POINTS, self._group, program)):
331
+ # Exit early if changing domain is not needed.
332
+ return
333
+
334
+ # Recreate vertex list.
335
+ self._vertex_list.delete()
336
+ self._create_vertex_list()
333
337
 
334
338
  def delete(self):
335
339
  """Force immediate removal of the sprite from video memory.
@@ -596,7 +600,6 @@ class Sprite(event.EventDispatcher):
596
600
  `scale_y` : float
597
601
  Vertical scaling factor.
598
602
  """
599
-
600
603
  translations_outdated = False
601
604
 
602
605
  # only bother updating if the translation actually changed
@@ -0,0 +1,124 @@
1
+ """Experimental multithreaded Job system.
2
+
3
+ This module provides the `JobExecutor` class, which allows submitting
4
+ batches of highly parallizable work in the form of functions and arguments.
5
+ This is commonly known as "task-based multithreading" or a "multithreaded
6
+ job system".
7
+
8
+ ..note:: This module is only really useful with the recent Python 3.13
9
+ releases that are built with experimental free-threading support.
10
+ With typical Python releases that contain a GIL, this is not of
11
+ much practical use.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+
18
+ from queue import Queue
19
+ from threading import Event, Thread
20
+ from typing import TYPE_CHECKING
21
+
22
+ if TYPE_CHECKING:
23
+ from typing import Callable, Any
24
+
25
+
26
+ class _Worker(Thread):
27
+ """A worker thread to pop and run jobs."""
28
+ def __init__(self, workqueue: Queue, exitevent: Event, name: str) -> None:
29
+ super().__init__(name=name, daemon=True)
30
+ self.queue = workqueue
31
+ self.exit = exitevent
32
+ self.start()
33
+
34
+ def run(self) -> None:
35
+ """Parallel thread of execution."""
36
+ _exit = self.exit
37
+ _queue = self.queue
38
+ while not _exit.is_set():
39
+ func, args = _queue.get()
40
+ func(*args)
41
+ _queue.task_done()
42
+
43
+ def __repr__(self) -> str:
44
+ return f"{self.name}(id={self.native_id})"
45
+
46
+
47
+ class JobExecutor:
48
+ """A light-weight multithreaded job system.
49
+
50
+ A JobExecutor loosely mimics the design of the Executor classes from the
51
+ `concurrent.futures` module, but does not share any code with those classes.
52
+ Instead, it is a more light-weight implementation intended for execution of
53
+ highly parallizable functions (jobs). A key difference is that JobExecutor
54
+ does not return `Futures`, and instead depends on the submitted jobs to
55
+ self-contained.
56
+ """
57
+
58
+ def __init__(self, max_workers: int | None = None) -> None:
59
+ """Create an instance of a JobExecutor.
60
+
61
+ Args:
62
+ max_workers: The number of threads to use. If `None`, will
63
+ create half as many threads as reported CPU cores.
64
+ """
65
+ self._max_workers = max_workers or os.cpu_count() // 2
66
+ self._exitevent = Event()
67
+ self._queue = Queue()
68
+ self._threads = [_Worker(self._queue, self._exitevent, f"Thread{i+1}") for i in range(self._max_workers)]
69
+
70
+ def submit(self, func: Callable, *args: Any) -> None:
71
+ """Submit a job to be executed on .
72
+
73
+ A "job" consists of a function to be called, alone with any arguments
74
+ that should be passed to it. Jobs are automatically executed by the next
75
+ free worker thread. No values are returned when jobs are submitted,
76
+ and the functions should also not return any values; return values are
77
+ lost.
78
+
79
+ Args:
80
+ func: A function to execute.
81
+ *args: Any arguments to pass to the function.
82
+ """
83
+ try:
84
+ self._queue.put((func, args))
85
+ except AttributeError:
86
+ raise RuntimeError("cannot submit new tasks after shutdown")
87
+
88
+ def shutdown(self) -> None:
89
+ """Shut down the JobExecutor, terminating all worker threads.
90
+
91
+ All JobExecutor workers are Daemon Threads, so it is not strictly
92
+ necessary to call shutdown() at program termination. However, if
93
+ it is no longer needed, shutdown() can be called ot free up the
94
+ thread resources.
95
+ """
96
+ if not self._queue:
97
+ return
98
+ self._exitevent.set()
99
+ for _ in range(self._max_workers):
100
+ self.submit(lambda: None)
101
+ self._threads.clear()
102
+ self._exitevent = None
103
+ self._queue = None
104
+
105
+ def sync(self) -> None:
106
+ """Wait for all currently submitted jobs to complete.
107
+
108
+ This method will wait until the internal queue is empty, AND all
109
+ currently submitted jobs have completed execution. It can be used
110
+ to fence between separate batches of jobs that should not be run
111
+ at the same time. For example::
112
+
113
+ for chunk in worklist:
114
+ executor.submit(some_function, chunk)
115
+
116
+ executor.sync()
117
+
118
+ window.draw()
119
+
120
+ """
121
+ self._queue.join()
122
+
123
+ def __repr__(self) -> str:
124
+ return f"{self.__class__.__name__}(workers={self._threads})"
@@ -1,13 +1,19 @@
1
1
  """Allows for a somewhat generic multitextured sprite.
2
2
 
3
3
  This sprite behaves just like regular sprites.
4
-
5
4
  """
6
5
  from __future__ import annotations
7
6
 
7
+ from typing import TYPE_CHECKING
8
+
8
9
  import pyglet
9
- from pyglet.gl import glActiveTexture, GL_TEXTURE0, glBindTexture, glEnable, GL_BLEND, glBlendFunc, glDisable, \
10
- glGetIntegerv, GLint, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_TRIANGLES, GL_MAX_TEXTURE_IMAGE_UNITS
10
+ from pyglet.gl import glActiveTexture, glBindTexture, glEnable, GL_BLEND, glBlendFunc, glDisable, glGetIntegerv, GLint
11
+ from pyglet.gl import GL_SRC_ALPHA, GL_TEXTURE0, GL_ONE_MINUS_SRC_ALPHA, GL_TRIANGLES, GL_MAX_TEXTURE_IMAGE_UNITS
12
+
13
+ if TYPE_CHECKING:
14
+ from pyglet.image import Texture, AbstractImage, Animation
15
+ from pyglet.graphics import Batch, Group, ShaderProgram
16
+
11
17
 
12
18
  class MultiTextureSpriteGroup(pyglet.graphics.Group):
13
19
  """Shared Multi-texture Sprite rendering Group.
@@ -104,6 +110,7 @@ class MultiTextureSpriteGroup(pyglet.graphics.Group):
104
110
  tuple([texture.id for texture in self._textures.values()]) +
105
111
  tuple([texture.target for texture in self._textures.values()]))
106
112
 
113
+
107
114
  # Allows the default shader to pick the appropriate sampler for the fragment shader
108
115
  _SAMPLER_TYPES = {
109
116
  pyglet.gl.GL_TEXTURE_2D: "sampler2D",
@@ -116,6 +123,7 @@ _SAMPLER_COORDS = {
116
123
  pyglet.gl.GL_TEXTURE_2D_ARRAY: ""
117
124
  }
118
125
 
126
+
119
127
  def _get_default_mt_shader(images: dict[str, Texture]) -> ShaderProgram:
120
128
  """Creates the default multi-texture shader based on the dict of textures passed in.
121
129
 
@@ -126,7 +134,7 @@ def _get_default_mt_shader(images: dict[str, Texture]) -> ShaderProgram:
126
134
  """
127
135
  max_tex = GLint()
128
136
  glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, max_tex)
129
- assert len(images) <= max_tex.value, f"The default multi-texture shader only supports up to a max of {max_tex.value} textures."
137
+ assert len(images) <= max_tex.value, f"Only {max_tex.value} Texture Units are available."
130
138
 
131
139
  # Generate the default vertex shader
132
140
  in_tex_coords = '\n'.join([f"in vec3 {name}_coords;" for name in images.keys()])
@@ -176,7 +184,7 @@ def _get_default_mt_shader(images: dict[str, Texture]) -> ShaderProgram:
176
184
 
177
185
  in_tex_coords = '\n'.join([f"in vec3 {name}_coords_frag;" for name in images.keys()])
178
186
  uniform_samplers = '\n'.join([f"uniform {_SAMPLER_TYPES[tex.target]} {name};" for name,tex in images.items()])
179
- tex_operations = '\n'.join([f" color = layer(texture({name}, {name}_coords_frag{_SAMPLER_COORDS[tex.target]}), color);" for name,tex in images.items()])
187
+ tex_operations = '\n'.join([f" color = layer(texture({name}, {name}_coords_frag{_SAMPLER_COORDS[tex.target]}), color);" for name, tex in images.items()])
180
188
  fragment_source = f"""
181
189
  #version 150 core
182
190
 
@@ -200,6 +208,7 @@ def _get_default_mt_shader(images: dict[str, Texture]) -> ShaderProgram:
200
208
 
201
209
  return pyglet.gl.current_context.create_program((vertex_source, 'vertex'), (fragment_source, 'fragment'))
202
210
 
211
+
203
212
  class MultiTextureSprite(pyglet.sprite.Sprite):
204
213
  """Creates a multi-textured sprite.
205
214
 
@@ -259,7 +268,7 @@ class MultiTextureSprite(pyglet.sprite.Sprite):
259
268
  Args:
260
269
  images:
261
270
  A dict object with the key being the name of the texture and the
262
- value is either an Animation or AbstractImage. Currently
271
+ value is either an Animation or AbstractImage. Currently,
263
272
  each item must be of the same size.
264
273
  x:
265
274
  X coordinate of the sprite.
@@ -280,7 +289,6 @@ class MultiTextureSprite(pyglet.sprite.Sprite):
280
289
  subpixel:
281
290
  Allow floating-point coordinates for the sprite. By default,
282
291
  coordinates are restricted to integer values.
283
-
284
292
  program:
285
293
  A specific shader program to initialize the sprite with. The
286
294
  default multi-texture overlay shader will be used if one is
@@ -392,7 +400,7 @@ class MultiTextureSprite(pyglet.sprite.Sprite):
392
400
  self._vertex_list = self._program.vertex_list_indexed(
393
401
  4, GL_TRIANGLES, [0, 1, 2, 0, 2, 3], self._batch, self._group,
394
402
  position=('f', self._get_vertices()),
395
- colors=('Bn', (*self._rgb, int(self._opacity)) * 4),
403
+ colors=('Bn', self._rgba * 4),
396
404
  translate=('f', (self._x, self._y, self._z) * 4),
397
405
  scale=('f', (self._scale * self._scale_x, self._scale * self._scale_y) * 4),
398
406
  rotation=('f', (self._rotation,) * 4),
@@ -455,6 +463,8 @@ class MultiTextureSprite(pyglet.sprite.Sprite):
455
463
  Args:
456
464
  name:
457
465
  The dict key given for the texture layer in the constructor
466
+ img:
467
+ The Image or Animation to set
458
468
  """
459
469
  if name in self._animations:
460
470
  # Need to stop all animations temporarly so we can swap the layer out
@@ -465,7 +475,7 @@ class MultiTextureSprite(pyglet.sprite.Sprite):
465
475
  tex = None
466
476
  if isinstance(img, pyglet.image.Animation):
467
477
  # Add the animation and schedule it based on pause
468
- self._animations[name] = { "animation": img, "frame_idx": 0, "next_dt": img.frames[0].duration }
478
+ self._animations[name] = {"animation": img, "frame_idx": 0, "next_dt": img.frames[0].duration}
469
479
  if img.frames[0].duration and not self._paused:
470
480
  pyglet.clock.schedule_once(self._animate, self._animations[name]["next_dt"], name)
471
481
 
@@ -481,7 +491,6 @@ class MultiTextureSprite(pyglet.sprite.Sprite):
481
491
  # Only update if we actually changed the "base" texture
482
492
  self._update_position()
483
493
 
484
-
485
494
  @property
486
495
  def frame_index(self) -> None:
487
496
  raise NotImplementedError("MultiTextureSprite does not support the frame_index property. Use get_frame_index instead.")
@@ -512,7 +521,7 @@ class MultiTextureSprite(pyglet.sprite.Sprite):
512
521
  if pause:
513
522
  pyglet.clock.unschedule(self._animate)
514
523
  else:
515
- # Kick off all of the animations again
524
+ # Kick off all animations again
516
525
  for name, animation in self._animations.items():
517
526
  frame = animation["animation"].frames[animation["frame_idx"]]
518
527
  if frame.duration:
@@ -6,12 +6,8 @@ import sys
6
6
  import time
7
7
 
8
8
  import pyglet
9
-
9
+ from pyglet import clock, event, graphics, image
10
10
  from pyglet.gl import *
11
- from pyglet import clock
12
- from pyglet import event
13
- from pyglet import graphics
14
- from pyglet import image
15
11
 
16
12
  _is_pyglet_doc_run = hasattr(sys, "is_pyglet_doc_run") and sys.is_pyglet_doc_run
17
13
 
@@ -299,8 +295,14 @@ class Emitter(event.EventDispatcher):
299
295
  self._group.blend_dest,
300
296
  program,
301
297
  self._user_group)
302
- self._batch.migrate(self._vertex_list, GL_POINTS, self._group, self._batch)
303
- self._program = program
298
+ if (self._batch and
299
+ self._batch.update_shader(self._vertex_list, GL_POINTS, self._group, program)):
300
+ # Exit early if changing domain is not needed.
301
+ return
302
+
303
+ # Recreate vertex list.
304
+ self._vertex_list.delete()
305
+ self._create_vertex_list()
304
306
 
305
307
  def delete(self):
306
308
  """Force immediate removal of the emitter from video memory.
@@ -407,7 +409,7 @@ class ParticleManager:
407
409
 
408
410
  # TODO: remove debug
409
411
  self.total_number -= 1
410
- self.total_label.text = f"particles: {str(self.total_number * self._count * 8)}"
412
+ self.total_label.text = f"particles: {self.total_number * self._count * 8!s}"
411
413
 
412
414
  def create_emitter(self, x, y, z=0):
413
415
  emitter = Emitter(self._img, x, y, z, self._count, self._velocity, self._spread,
@@ -418,6 +420,6 @@ class ParticleManager:
418
420
 
419
421
  # TODO: remove debug
420
422
  self.total_number += 1
421
- self.total_label.text = f"particles: {str(self.total_number * self._count * 8)}"
423
+ self.total_label.text = f"particles: {self.total_number * self._count * 8!s}"
422
424
 
423
425
  return emitter
pyglet/gl/__init__.py CHANGED
@@ -162,16 +162,18 @@ def _create_shadow_window() -> None:
162
162
  from pyglet.window import Window
163
163
 
164
164
  class ShadowWindow(Window):
165
+ _shadow = True
165
166
  def __init__(self) -> None:
166
167
  super().__init__(width=1, height=1, visible=False)
167
168
 
168
169
  def _create_projection(self) -> None:
169
170
  """Shadow window does not need a projection."""
170
- pass
171
171
 
172
- def on_resize(self, width, height):
172
+ def _on_internal_resize(self, width: int, height: int) -> None:
173
+ """No projection and not required."""
174
+
175
+ def _on_internal_scale(self, scale: float, dpi: int) -> None:
173
176
  """No projection and not required."""
174
- pass
175
177
 
176
178
  _shadow_window = ShadowWindow()
177
179
  _shadow_window.switch_to()