pyglet 2.1.8__py3-none-any.whl → 2.1.10__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.
pyglet/__init__.py CHANGED
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
15
15
  from typing import Any, Callable, ItemsView, Sized
16
16
 
17
17
  #: The release version
18
- version = '2.1.8'
18
+ version = '2.1.10'
19
19
  __version__ = version
20
20
 
21
21
  MIN_PYTHON_VERSION = 3, 8
@@ -255,8 +255,8 @@ class Options:
255
255
 
256
256
  .. versionadded:: 2.0.5"""
257
257
 
258
- dpi_scaling: Literal["real", "scaled", "stretch", "platform"] = "real"
259
- """For 'HiDPI' displays, Window behavior can differ between operating systems. Defaults to `'real'`.
258
+ dpi_scaling: Literal["real", "scaled", "stretch", "platform"] = "platform"
259
+ """For 'HiDPI' displays, Window behavior can differ between operating systems. Defaults to `'platform'`.
260
260
 
261
261
  The current options are an attempt to create consistent behavior across all of the operating systems.
262
262
 
pyglet/app/cocoa.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import signal
2
4
  import time
3
5
 
@@ -1,3 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
1
5
  import pyglet
2
6
  import warnings
3
7
 
@@ -69,3 +73,10 @@ class HeadlessScreen(Screen):
69
73
 
70
74
  def restore_mode(self):
71
75
  pass
76
+
77
+ def get_display_id(self) -> str | int:
78
+ # No real unique ID is available, just hash together the properties.
79
+ return hash((self.x, self.y, self.width, self.height))
80
+
81
+ def get_monitor_name(self) -> str | Literal["Unknown"]:
82
+ return "Headless"
pyglet/graphics/shader.py CHANGED
@@ -58,6 +58,7 @@ from pyglet.gl.gl import (
58
58
  glUnmapBuffer,
59
59
  glUseProgram,
60
60
  glVertexAttribDivisor,
61
+ glVertexAttribIPointer,
61
62
  glVertexAttribPointer,
62
63
  )
63
64
  from pyglet.graphics.vertexbuffer import AttributeBufferObject, BufferObject
@@ -158,7 +159,7 @@ _uniform_setters: dict[int, tuple[GLDataType, GLFunc, GLFunc, int]] = {
158
159
  gl.GL_UNSIGNED_INT_SAMPLER_3D: (gl.GLint, gl.glUniform1iv, gl.glProgramUniform1iv, 1),
159
160
 
160
161
  gl.GL_FLOAT_MAT2: (gl.GLfloat, gl.glUniformMatrix2fv, gl.glProgramUniformMatrix2fv, 4),
161
- gl.GL_FLOAT_MAT3: (gl.GLfloat, gl.glUniformMatrix3fv, gl.glProgramUniformMatrix3fv, 6),
162
+ gl.GL_FLOAT_MAT3: (gl.GLfloat, gl.glUniformMatrix3fv, gl.glProgramUniformMatrix3fv, 9),
162
163
  gl.GL_FLOAT_MAT4: (gl.GLfloat, gl.glUniformMatrix4fv, gl.glProgramUniformMatrix4fv, 16),
163
164
 
164
165
  # TODO: test/implement these:
@@ -257,6 +258,9 @@ class Attribute:
257
258
  self.element_size = sizeof(self.c_type)
258
259
  self.stride = count * self.element_size
259
260
 
261
+ self._is_int = gl_type in (gl.GL_INT, gl.GL_SHORT, gl.GL_BYTE, gl.GL_UNSIGNED_INT,
262
+ gl.GL_UNSIGNED_SHORT, gl.GL_UNSIGNED_BYTE) and self.normalize is False
263
+
260
264
  def enable(self) -> None:
261
265
  """Enable the attribute."""
262
266
  glEnableVertexAttribArray(self.location)
@@ -271,7 +275,10 @@ class Attribute:
271
275
  Pointer offset to the currently bound buffer for this attribute.
272
276
 
273
277
  """
274
- glVertexAttribPointer(self.location, self.count, self.gl_type, self.normalize, self.stride, ptr)
278
+ if self._is_int:
279
+ glVertexAttribIPointer(self.location, self.count, self.gl_type, self.stride, ptr)
280
+ else:
281
+ glVertexAttribPointer(self.location, self.count, self.gl_type, self.normalize, self.stride, ptr)
275
282
 
276
283
  def set_divisor(self) -> None:
277
284
  glVertexAttribDivisor(self.location, 1)
pyglet/input/base.py CHANGED
@@ -520,6 +520,7 @@ class Controller(EventDispatcher):
520
520
  #: The unique guid for this Device
521
521
  self.guid: str = mapping.get('guid')
522
522
 
523
+ # Pollable
523
524
  self.a: bool = False
524
525
  self.b: bool = False
525
526
  self.x: bool = False
@@ -534,6 +535,10 @@ class Controller(EventDispatcher):
534
535
 
535
536
  self.lefttrigger: float = 0.0
536
537
  self.righttrigger: float = 0.0
538
+ self.dpad: Vec2 = Vec2()
539
+ self.leftanalog: Vec2 = Vec2()
540
+ self.rightanalog: Vec2 = Vec2()
541
+
537
542
  self.leftx: float = 0.0
538
543
  self.lefty: float = 0.0
539
544
  self.rightx: float = 0.0
@@ -617,12 +622,14 @@ class Controller(EventDispatcher):
617
622
  @control.event
618
623
  def on_change(value):
619
624
  self.dpady = round(value * scale + bias) * sign # normalized
625
+ self.dpad = Vec2(self.dpadx, self.dpady)
620
626
  self.dispatch_event('on_dpad_motion', self, Vec2(self.dpadx, self.dpady))
621
627
 
622
628
  elif axis_name in ("dpleft", "dpright"):
623
629
  @control.event
624
630
  def on_change(value):
625
631
  self.dpadx = round(value * scale + bias) * sign # normalized
632
+ self.dpad = Vec2(self.dpadx, self.dpady)
626
633
  self.dispatch_event('on_dpad_motion', self, Vec2(self.dpadx, self.dpady))
627
634
 
628
635
  elif axis_name in ("lefttrigger", "righttrigger"):
@@ -637,14 +644,16 @@ class Controller(EventDispatcher):
637
644
  def on_change(value):
638
645
  normalized_value = value * scale + bias
639
646
  setattr(self, axis_name, normalized_value)
640
- self.dispatch_event('on_stick_motion', self, "leftstick", Vec2(self.leftx, -self.lefty))
647
+ self.left_analog = Vec2(self.leftx, -self.lefty)
648
+ self.dispatch_event('on_stick_motion', self, "leftstick", self.left_analog)
641
649
 
642
650
  elif axis_name in ("rightx", "righty"):
643
651
  @control.event
644
652
  def on_change(value):
645
653
  normalized_value = value * scale + bias
646
654
  setattr(self, axis_name, normalized_value)
647
- self.dispatch_event('on_stick_motion', self, "rightstick", Vec2(self.rightx, -self.righty))
655
+ self.right_analog = Vec2(self.rightx, -self.righty)
656
+ self.dispatch_event('on_stick_motion', self, "rightstick", self.right_analog)
648
657
 
649
658
  def _bind_button_control(self, relation: Relation, control: Button, button_name: str) -> None:
650
659
  if button_name in ("dpleft", "dpright", "dpup", "dpdown"):
@@ -655,7 +664,8 @@ class Controller(EventDispatcher):
655
664
  def on_change(value):
656
665
  target, bias = defaults[button_name]
657
666
  setattr(self, target, bias * value)
658
- self.dispatch_event('on_dpad_motion', self, Vec2(self.dpadx, self.dpady))
667
+ self.dpad = Vec2(self.dpadx, self.dpady)
668
+ self.dispatch_event('on_dpad_motion', self, self.dpad)
659
669
  else:
660
670
  @control.event
661
671
  def on_change(value):
@@ -681,7 +691,7 @@ class Controller(EventDispatcher):
681
691
  @control.event
682
692
  def on_change(value):
683
693
  vector = _input_map.get(value // _scale, Vec2(0.0, 0.0))
684
- self.dpadx, self.dpady = vector
694
+ self.dpad = vector
685
695
  self.dispatch_event('on_dpad_motion', self, vector)
686
696
 
687
697
  def _initialize_controls(self) -> None:
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import os
4
4
  import time
5
5
  import ctypes
6
+ import select
6
7
  import warnings
7
8
  import threading
8
9
 
@@ -11,6 +12,7 @@ from ctypes import c_int16 as _s16
11
12
  from ctypes import c_uint32 as _u32
12
13
  from ctypes import c_int32 as _s32
13
14
  from ctypes import c_int64 as _s64
15
+ from ctypes import c_byte as _c_byte
14
16
 
15
17
  import pyglet
16
18
 
@@ -31,6 +33,26 @@ except ImportError:
31
33
  return c.read(fd, buffers, 3072)
32
34
 
33
35
 
36
+ KeyMaxArray = _c_byte * (KEY_MAX // 8 + 1)
37
+
38
+
39
+ class _EvdevInfo:
40
+ event_type: int
41
+ event_code: int
42
+
43
+
44
+ class EvdevButton(Button, _EvdevInfo):
45
+ pass
46
+
47
+
48
+ class EvdevAbsoluteAxis(AbsoluteAxis, _EvdevInfo):
49
+ pass
50
+
51
+
52
+ class EvdevRelativeAxis(RelativeAxis, _EvdevInfo):
53
+ pass
54
+
55
+
34
56
  # Structures from /linux/blob/master/include/uapi/linux/input.h
35
57
 
36
58
  class Timeval(ctypes.Structure):
@@ -158,11 +180,13 @@ class FFEvent(ctypes.Structure):
158
180
  )
159
181
 
160
182
 
183
+ # Helper "macros" for file io:
161
184
  EVIOCGVERSION = _IOR('E', 0x01, ctypes.c_int)
162
185
  EVIOCGID = _IOR('E', 0x02, InputID)
163
186
  EVIOCGNAME = _IOR_str('E', 0x06)
164
187
  EVIOCGPHYS = _IOR_str('E', 0x07)
165
188
  EVIOCGUNIQ = _IOR_str('E', 0x08)
189
+ EVIOCGKEY = _IOR_len('E', 0x18)
166
190
  EVIOCSFF = _IOW('E', 0x80, FFEvent)
167
191
 
168
192
 
@@ -170,9 +194,13 @@ def EVIOCGBIT(fileno, ev, buffer):
170
194
  return _IOR_len('E', 0x20 + ev)(fileno, buffer)
171
195
 
172
196
 
173
- def EVIOCGABS(fileno, abs):
174
- buffer = InputABSInfo()
175
- return _IOR_len('E', 0x40 + abs)(fileno, buffer)
197
+ def EVIOCGABS(fileno, ev, buffer=InputABSInfo()):
198
+ return _IOR_len('E', 0x40 + ev)(fileno, buffer)
199
+
200
+
201
+ def get_key_state(fileno, event_code, buffer=KeyMaxArray()):
202
+ buffer = EVIOCGKEY(fileno, buffer)
203
+ return bool(buffer[event_code // 8] & (1 << (event_code % 8)))
176
204
 
177
205
 
178
206
  def get_set_bits(bytestring):
@@ -217,18 +245,16 @@ def _create_control(fileno, event_type, event_code):
217
245
  value = absinfo.value
218
246
  minimum = absinfo.minimum
219
247
  maximum = absinfo.maximum
220
- control = AbsoluteAxis(name, minimum, maximum, raw_name)
248
+ control = EvdevAbsoluteAxis(name, minimum, maximum, raw_name, inverted=name == 'hat_y')
221
249
  control.value = value
222
- if name == 'hat_y':
223
- control.inverted = True
224
250
  elif event_type == EV_REL:
225
251
  raw_name = rel_raw_names.get(event_code, f'EV_REL({event_code:x})')
226
252
  name = _rel_names.get(event_code)
227
- control = RelativeAxis(name, raw_name)
253
+ control = EvdevRelativeAxis(name, raw_name)
228
254
  elif event_type == EV_KEY:
229
255
  raw_name = key_raw_names.get(event_code, f'EV_KEY({event_code:x})')
230
256
  name = None
231
- control = Button(name, raw_name)
257
+ control = EvdevButton(name, raw_name)
232
258
  else:
233
259
  return None
234
260
  control.event_type = event_type
@@ -248,7 +274,8 @@ event_types = {
248
274
 
249
275
 
250
276
  class EvdevDevice(XlibSelectDevice, Device):
251
- _fileno = None
277
+ _fileno: int | None
278
+ _poll: "select.poll | None"
252
279
 
253
280
  def __init__(self, display, filename):
254
281
  self._filename = filename
@@ -301,11 +328,14 @@ class EvdevDevice(XlibSelectDevice, Device):
301
328
  self.control_map[(event_type, event_code)] = control
302
329
  self.controls.append(control)
303
330
 
304
- self.controls.sort(key=lambda c: c.event_code)
331
+ self.controls.sort(key=lambda ctrl: ctrl.event_code)
305
332
  os.close(fileno)
306
333
 
334
+ self._poll = select.poll()
307
335
  self._event_size = ctypes.sizeof(InputEvent)
308
336
  self._event_buffer = (InputEvent * 64)()
337
+ self._syn_dropped = False
338
+ self._event_queue = []
309
339
 
310
340
  super().__init__(display, name)
311
341
 
@@ -321,6 +351,7 @@ class EvdevDevice(XlibSelectDevice, Device):
321
351
  def open(self, window=None, exclusive=False):
322
352
  try:
323
353
  self._fileno = os.open(self._filename, os.O_RDWR | os.O_NONBLOCK)
354
+ self._poll.register(self._fileno, select.POLLIN | select.POLLPRI)
324
355
  except OSError as e:
325
356
  raise DeviceOpenException(e)
326
357
 
@@ -333,6 +364,9 @@ class EvdevDevice(XlibSelectDevice, Device):
333
364
  if not self._fileno:
334
365
  return
335
366
 
367
+ if self._poll:
368
+ self._poll.unregister(self._fileno)
369
+
336
370
  pyglet.app.platform_event_loop.select_devices.remove(self)
337
371
  os.close(self._fileno)
338
372
  self._fileno = None
@@ -340,6 +374,20 @@ class EvdevDevice(XlibSelectDevice, Device):
340
374
  def get_controls(self):
341
375
  return self.controls
342
376
 
377
+ def _resync_control_state(self):
378
+ """Manually resync all Control state.
379
+
380
+ This method queries and resets the state of each Control using the appropriate
381
+ ioctl calls. If this causes the Control value to change, the associated events
382
+ will be dispatched. This is a somewhat expensive operation, but it is necessary
383
+ to perform in some cases (such as when a SYN_DROPPED event is received).
384
+ """
385
+ for control in self.control_map.values():
386
+ if isinstance(control, EvdevButton):
387
+ control.value = get_key_state(self._fileno, control.event_code)
388
+ if isinstance(control, EvdevAbsoluteAxis):
389
+ control.value = EVIOCGABS(self._fileno, control.event_code).value
390
+
343
391
  # Force Feedback methods
344
392
 
345
393
  def ff_upload_effect(self, structure):
@@ -351,25 +399,51 @@ class EvdevDevice(XlibSelectDevice, Device):
351
399
  return self._fileno
352
400
 
353
401
  def poll(self):
354
- return False
402
+ return True if self._poll.poll(0) else False
355
403
 
356
404
  def select(self):
357
- if not self._fileno:
358
- return
359
-
405
+ """When the file descriptor is ready, read and process InputEvents.
406
+
407
+ This method has the following behavior:
408
+ - Read and queue all incoming input events.
409
+ - When a SYN_REPORT event is received, dispatch all queued events.
410
+ - If a SYN_DROPPED event is received, set a flag. When the next
411
+ SYN_REPORT event appears, drop all queued events & manually resync
412
+ all Control state.
413
+ """
360
414
  try:
361
415
  bytes_read = _readv(self._fileno, self._event_buffer)
416
+ n_events = bytes_read // self._event_size
362
417
  except OSError:
363
418
  self.close()
364
419
  return
365
420
 
366
- n_events = bytes_read // self._event_size
367
-
368
421
  for event in self._event_buffer[:n_events]:
369
- try:
370
- self.control_map[(event.type, event.code)].value = event.value
371
- except KeyError:
372
- pass
422
+
423
+ # Mark the current chain of events as invalid and continue:
424
+ if (event.type, event.code) == (EV_SYN, SYN_DROPPED):
425
+ self._syn_dropped = True
426
+ continue
427
+
428
+ # Dispatch queued events when SYN_REPORT comes in:
429
+ if (event.type, event.code) == (EV_SYN, SYN_REPORT):
430
+
431
+ # Unless a SYN_DROPPED event has been received,
432
+ # in which case discard all queued events and resync:
433
+ if self._syn_dropped:
434
+ self._event_queue.clear()
435
+ self._syn_dropped = False
436
+ self._resync_control_state()
437
+
438
+ # Dispatch all queued events, then clear the queue:
439
+ for queued_event in self._event_queue:
440
+ if control := self.control_map.get((queued_event.type, queued_event.code)):
441
+ control.value = queued_event.value
442
+ self._event_queue.clear()
443
+
444
+ # This is not a SYN_REPORT or SYN_DROPPED event, so it is probably
445
+ # an input event. Queue it until the next SYN_REPORT event comes in:
446
+ self._event_queue.append(event)
373
447
 
374
448
 
375
449
  class FFController(Controller):
@@ -467,10 +541,9 @@ class EvdevControllerManager(ControllerManager, XlibSelectDevice):
467
541
  else:
468
542
  return # No device could be created
469
543
 
470
- # Reuse existing controller instance if it exists, or create a new one:
471
- if controller := self._controllers.get(name, _create_controller(device)):
544
+ if controller := _create_controller(device):
472
545
  self._controllers[name] = controller
473
- # Dispatch event in main thread:
546
+ # Post the event in the main thread:
474
547
  self.post_event('on_connect', controller)
475
548
 
476
549
  def select(self):
@@ -486,6 +559,7 @@ class EvdevControllerManager(ControllerManager, XlibSelectDevice):
486
559
 
487
560
  for name in disappeared:
488
561
  if controller := self._controllers.get(name):
562
+ del self._controllers[name]
489
563
  self.dispatch_event('on_disconnect', controller)
490
564
 
491
565
  def get_controllers(self) -> list[Controller]:
@@ -551,8 +625,8 @@ def _detect_controller_mapping(device):
551
625
  ABS_Z: 'lefttrigger', ABS_RZ: 'righttrigger',
552
626
  ABS_X: 'leftx', ABS_Y: 'lefty', ABS_RX: 'rightx', ABS_RY: 'righty'}
553
627
 
554
- button_controls = [control for control in device.controls if isinstance(control, Button)]
555
- axis_controls = [control for control in device.controls if isinstance(control, AbsoluteAxis)]
628
+ button_controls = [control for control in device.controls if isinstance(control, EvdevButton)]
629
+ axis_controls = [control for control in device.controls if isinstance(control, EvdevAbsoluteAxis)]
556
630
  hat_controls = [control for control in device.controls if control.name in ('hat_x', 'hat_y')]
557
631
 
558
632
  for i, control in enumerate(button_controls):
@@ -1,4 +1,4 @@
1
- """Event constants from /usr/include/linux/input.h """
1
+ """Event constants from /usr/include/linux/input-event-codes.h"""
2
2
 
3
3
  EV_SYN = 0x00
4
4
  EV_KEY = 0x01
@@ -17,6 +17,7 @@ EV_MAX = 0x1f
17
17
 
18
18
  SYN_REPORT = 0
19
19
  SYN_CONFIG = 1
20
+ SYN_DROPPED = 3
20
21
 
21
22
  # Keys and buttons
22
23
 
@@ -575,14 +575,16 @@ class XInputController(Controller):
575
575
  def on_change(value):
576
576
  normalized_value = value * scale + bias
577
577
  setattr(self, name, normalized_value)
578
- self.dispatch_event('on_stick_motion', self, "leftstick", Vec2(self.leftx, self.lefty))
578
+ self.leftanalog = Vec2(self.leftx, self.lefty)
579
+ self.dispatch_event('on_stick_motion', self, "leftstick", self.leftanalog)
579
580
 
580
581
  elif name in ("rightx", "righty"):
581
582
  @control.event
582
583
  def on_change(value):
583
584
  normalized_value = value * scale + bias
584
585
  setattr(self, name, normalized_value)
585
- self.dispatch_event('on_stick_motion', self, "rightstick", Vec2(self.rightx, self.righty))
586
+ self.rightanalog = Vec2(self.rightx, self.righty)
587
+ self.dispatch_event('on_stick_motion', self, "rightstick", self.rightanalog)
586
588
 
587
589
  def _add_button(self, control, name):
588
590
 
@@ -592,7 +594,8 @@ class XInputController(Controller):
592
594
  target, bias = {'dpleft': ('dpadx', -1.0), 'dpright': ('dpadx', 1.0),
593
595
  'dpdown': ('dpady', -1.0), 'dpup': ('dpady', 1.0)}[name]
594
596
  setattr(self, target, bias * value)
595
- self.dispatch_event('on_dpad_motion', self, Vec2(self.dpadx, self.dpady))
597
+ self.dpad = Vec2(self.dpadx, self.dpady)
598
+ self.dispatch_event('on_dpad_motion', self, self.dpad)
596
599
  else:
597
600
  @control.event
598
601
  def on_change(value):
pyglet/libs/ioctl.py CHANGED
@@ -41,7 +41,7 @@ from typing import TYPE_CHECKING
41
41
  if TYPE_CHECKING:
42
42
  from ctypes import Structure, c_int, c_uint
43
43
  from typing import Callable, Union
44
- c_data = Union[type[Structure], c_int, c_uint]
44
+ c_data = type[Structure] | type[c_int] | type[c_uint]
45
45
 
46
46
 
47
47
  _IOC_NRBITS = 8
@@ -64,7 +64,7 @@ _IOC_READ = 2
64
64
  # 'code' instead of 'type' to indicate the ioctl "magic number" ('H', 'E', etc.).
65
65
 
66
66
 
67
- def _IOC(io_dir: _IOC_NONE | _IOC_READ | _IOC_WRITE, code: int, nr: int, size: int) -> int:
67
+ def _IOC(io_dir: Union[_IOC_NONE, _IOC_READ, _IOC_WRITE], code: int, nr: int, size: int) -> int:
68
68
  return (io_dir << _IOC_DIRSHIFT) | (code << _IOC_TYPESHIFT) | (nr << _IOC_NRSHIFT) | (size << _IOC_SIZESHIFT)
69
69
 
70
70
 
pyglet/math.py CHANGED
@@ -1387,7 +1387,10 @@ class Mat4(_typing.NamedTuple):
1387
1387
 
1388
1388
 
1389
1389
  class Quaternion(_typing.NamedTuple):
1390
- """Quaternion."""
1390
+ """Quaternion.
1391
+
1392
+ Quaternions are 4-dimensional complex numbers, useful for describing 3D rotations.
1393
+ """
1391
1394
 
1392
1395
  w: float = 1.0
1393
1396
  x: float = 0.0
@@ -1403,6 +1406,8 @@ class Quaternion(_typing.NamedTuple):
1403
1406
  raise NotImplementedError
1404
1407
 
1405
1408
  def to_mat4(self) -> Mat4:
1409
+ """Calculate a 4x4 transform matrix which applies a rotation."""
1410
+
1406
1411
  w = self.w
1407
1412
  x = self.x
1408
1413
  y = self.y
@@ -1428,6 +1433,8 @@ class Quaternion(_typing.NamedTuple):
1428
1433
  return Mat4(a, b, c, 0.0, e, f, g, 0.0, i, j, k, 0.0, 0.0, 0.0, 0.0, 1.0)
1429
1434
 
1430
1435
  def to_mat3(self) -> Mat3:
1436
+ """Create a 3x3 rotation matrix."""
1437
+
1431
1438
  w = self.w
1432
1439
  x = self.x
1433
1440
  y = self.y
@@ -1453,21 +1460,34 @@ class Quaternion(_typing.NamedTuple):
1453
1460
  return Mat3(*(a, b, c, e, f, g, i, j, k))
1454
1461
 
1455
1462
  def length(self) -> float:
1456
- """Calculate the length of the Quaternion.
1457
-
1458
- The distance between the coordinates and the origin.
1459
- """
1463
+ """Calculate the length of the quaternion from the origin."""
1460
1464
  return _math.sqrt(self.w**2 + self.x**2 + self.y**2 + self.z**2)
1461
1465
 
1462
1466
  def conjugate(self) -> Quaternion:
1467
+ """Calculate the conjugate of this quaternion.
1468
+
1469
+ This operation:
1470
+ #. leaves the :py:attr:`.w` component alone
1471
+ #. inverts the sign of the :py:attr:`.x`, :py:attr:`.y`, and :py:attr:`.z` components
1472
+
1473
+ """
1463
1474
  return Quaternion(self.w, -self.x, -self.y, -self.z)
1464
1475
 
1465
1476
  def dot(self, other: Quaternion) -> float:
1477
+ """Calculate the dot product with another quaternion."""
1466
1478
  a, b, c, d = self
1467
1479
  e, f, g, h = other
1468
1480
  return a * e + b * f + c * g + d * h
1469
1481
 
1470
1482
  def normalize(self) -> Quaternion:
1483
+ """Calculate a unit quaternion from the instance.
1484
+
1485
+ The returned quaternion will be a scaled-down version
1486
+ of the instance which has:
1487
+
1488
+ * a length of ``1``
1489
+ * the same relative of its components
1490
+ """
1471
1491
  m = self.length()
1472
1492
  if m == 0:
1473
1493
  return self
@@ -1,9 +1,11 @@
1
+ from __future__ import annotations
2
+
1
3
  import ctypes
2
4
  import io
5
+ from dataclasses import dataclass
3
6
  from typing import TYPE_CHECKING, BinaryIO, List, Optional, Union
4
7
 
5
- from pyglet.media.exceptions import MediaException, CannotSeekException
6
- from pyglet.util import next_or_equal_power_of_two
8
+ from pyglet.media.exceptions import CannotSeekException, MediaException
7
9
 
8
10
  if TYPE_CHECKING:
9
11
  from pyglet.image import AbstractImage
@@ -77,7 +79,7 @@ class AudioFormat:
77
79
  self.__class__.__name__, self.channels, self.sample_size,
78
80
  self.sample_rate)
79
81
 
80
-
82
+ @dataclass
81
83
  class VideoFormat:
82
84
  """Video details.
83
85
 
@@ -98,20 +100,10 @@ class VideoFormat:
98
100
 
99
101
  .. versionadded:: 1.2
100
102
  """
101
-
102
- def __init__(self, width: int, height: int, sample_aspect: float = 1.0) -> None:
103
- self.width = width
104
- self.height = height
105
- self.sample_aspect = sample_aspect
106
- self.frame_rate = None
107
-
108
- def __eq__(self, other) -> bool:
109
- if isinstance(other, VideoFormat):
110
- return (self.width == other.width and
111
- self.height == other.height and
112
- self.sample_aspect == other.sample_aspect and
113
- self.frame_rate == other.frame_rate)
114
- return False
103
+ width: int
104
+ height: int
105
+ sample_aspect: float = 0.0
106
+ frame_rate: float | None = None
115
107
 
116
108
 
117
109
  class AudioData:
@@ -132,14 +124,14 @@ class AudioData:
132
124
  `timestamp` and `duration` are unused and will be removed eventually.
133
125
  """
134
126
 
135
- __slots__ = 'data', 'length', 'timestamp', 'duration', 'events', 'pointer'
127
+ __slots__ = 'data', 'duration', 'events', 'length', 'pointer', 'timestamp'
136
128
 
137
129
  def __init__(self,
138
- data: Union[bytes, ctypes.Array],
130
+ data: bytes | ctypes.Array,
139
131
  length: int,
140
132
  timestamp: float = 0.0,
141
133
  duration: float = 0.0,
142
- events: Optional[List['MediaEvent']] = None) -> None:
134
+ events: list[MediaEvent] | None = None) -> None:
143
135
 
144
136
  if isinstance(data, bytes):
145
137
  # bytes are treated specially by ctypes and can be cast to a void pointer, get
@@ -163,6 +155,7 @@ class AudioData:
163
155
  self.events = [] if events is None else events
164
156
 
165
157
 
158
+ @dataclass
166
159
  class SourceInfo:
167
160
  """Source metadata information.
168
161
 
@@ -180,15 +173,14 @@ class SourceInfo:
180
173
 
181
174
  .. versionadded:: 1.2
182
175
  """
183
-
184
- title = ''
185
- author = ''
186
- copyright = ''
187
- comment = ''
188
- album = ''
189
- year = 0
190
- track = 0
191
- genre = ''
176
+ title: str = ''
177
+ author: str = ''
178
+ copyright: str = ''
179
+ comment: str = ''
180
+ album: str = ''
181
+ year: int = 0
182
+ track: int = 0
183
+ genre: str = ''
192
184
 
193
185
 
194
186
  class Source:
@@ -255,9 +247,8 @@ class Source:
255
247
  player.on_player_eos = _on_player_eos
256
248
  return player
257
249
 
258
- def get_animation(self) -> 'Animation':
259
- """
260
- Import all video frames into memory.
250
+ def get_animation(self) -> Animation:
251
+ """Import all video frames into memory.
261
252
 
262
253
  An empty animation will be returned if the source has no video.
263
254
  Otherwise, the animation will contain all unplayed video frames (the