diff --git a/libs/pyglet/__init__.py b/libs/pyglet/__init__.py index be70285..ba3e362 100644 --- a/libs/pyglet/__init__.py +++ b/libs/pyglet/__init__.py @@ -44,11 +44,14 @@ import sys from typing import TYPE_CHECKING #: The release version -version = '2.0.dev20' +version = '2.0.dev22' __version__ = version -if sys.version_info < (3, 6): - raise Exception('pyglet %s requires Python 3.6 or newer.' % version) +MIN_PYTHON_VERSION = 3, 7 +MIN_PYTHON_VERSION_STR = '.'.join([str(v) for v in MIN_PYTHON_VERSION]) + +if sys.version_info < MIN_PYTHON_VERSION: + raise Exception(f"pyglet {version} requires Python {MIN_PYTHON_VERSION_STR} or newer.") if 'sphinx' in sys.modules: setattr(sys, 'is_pyglet_doc_run', True) diff --git a/libs/pyglet/canvas/win32.py b/libs/pyglet/canvas/win32.py index 1294bf0..4528644 100644 --- a/libs/pyglet/canvas/win32.py +++ b/libs/pyglet/canvas/win32.py @@ -65,11 +65,13 @@ class Win32Screen(Screen): self._handle = handle def get_matching_configs(self, template): - canvas = Win32Canvas(self.display, 0, _user32.GetDC(0)) + hdc = _user32.GetDC(0) + canvas = Win32Canvas(self.display, 0, hdc) configs = template.match(canvas) # XXX deprecate config's being screen-specific for config in configs: config.screen = self + _user32.ReleaseDC(0, hdc) return configs def get_device_name(self): diff --git a/libs/pyglet/font/directwrite.py b/libs/pyglet/font/directwrite.py index a733885..0c84a77 100644 --- a/libs/pyglet/font/directwrite.py +++ b/libs/pyglet/font/directwrite.py @@ -35,6 +35,7 @@ except OSError as err: _debug_font = debug_print('debug_font') + def DWRITE_MAKE_OPENTYPE_TAG(a, b, c, d): return ord(d) << 24 | ord(c) << 16 | ord(b) << 8 | ord(a) @@ -121,13 +122,13 @@ DWRITE_MEASURING_MODE_GDI_CLASSIC = 1 DWRITE_MEASURING_MODE_GDI_NATURAL = 2 DWRITE_GLYPH_IMAGE_FORMATS_ALL = DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE | \ - DWRITE_GLYPH_IMAGE_FORMATS_CFF | \ - DWRITE_GLYPH_IMAGE_FORMATS_COLR | \ - DWRITE_GLYPH_IMAGE_FORMATS_SVG | \ - DWRITE_GLYPH_IMAGE_FORMATS_PNG | \ - DWRITE_GLYPH_IMAGE_FORMATS_JPEG | \ - DWRITE_GLYPH_IMAGE_FORMATS_TIFF | \ - DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8 + DWRITE_GLYPH_IMAGE_FORMATS_CFF | \ + DWRITE_GLYPH_IMAGE_FORMATS_COLR | \ + DWRITE_GLYPH_IMAGE_FORMATS_SVG | \ + DWRITE_GLYPH_IMAGE_FORMATS_PNG | \ + DWRITE_GLYPH_IMAGE_FORMATS_JPEG | \ + DWRITE_GLYPH_IMAGE_FORMATS_TIFF | \ + DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8 DWRITE_FONT_STYLE = UINT DWRITE_FONT_STYLE_NORMAL = 0 @@ -171,8 +172,6 @@ class D2D1_COLOR_F(Structure): ) - - class DWRITE_TEXT_METRICS(ctypes.Structure): _fields_ = ( ('left', FLOAT), @@ -316,6 +315,7 @@ class DWRITE_GLYPH_RUN(ctypes.Structure): ('bidiLevel', UINT32), ) + DWRITE_SCRIPT_SHAPES = UINT DWRITE_SCRIPT_SHAPES_DEFAULT = 0 @@ -1061,6 +1061,21 @@ class LegacyCollectionLoader(com.COMObject): IID_IDWriteFactory = com.GUID(0xb859ee5a, 0xd838, 0x4b5b, 0xa2, 0xe8, 0x1a, 0xdc, 0x7d, 0x93, 0xdb, 0x48) +class IDWriteRenderingParams(com.pIUnknown): + _methods_ = [ + ('GetGamma', + com.METHOD(FLOAT)), + ('GetEnhancedContrast', + com.METHOD(FLOAT)), + ('GetClearTypeLevel', + com.METHOD(FLOAT)), + ('GetPixelGeometry', + com.METHOD(UINT)), + ('GetRenderingMode', + com.METHOD(UINT)), + ] + + class IDWriteFactory(com.pIUnknown): _methods_ = [ ('GetSystemFontCollection', @@ -1078,11 +1093,11 @@ class IDWriteFactory(com.pIUnknown): ('CreateFontFace', com.STDMETHOD()), ('CreateRenderingParams', - com.STDMETHOD()), + com.STDMETHOD(POINTER(IDWriteRenderingParams))), ('CreateMonitorRenderingParams', com.STDMETHOD()), ('CreateCustomRenderingParams', - com.STDMETHOD()), + com.STDMETHOD(FLOAT, FLOAT, FLOAT, UINT, UINT, POINTER(IDWriteRenderingParams))), ('RegisterFontFileLoader', com.STDMETHOD(c_void_p)), # Ambigious as newer is a pIUnknown and legacy is IUnknown. ('UnregisterFontFileLoader', @@ -1116,7 +1131,7 @@ class IDWriteFactory1(IDWriteFactory, com.pIUnknown): _methods_ = [ ('GetEudcFontCollection', com.STDMETHOD()), - ('CreateCustomRenderingParams', + ('CreateCustomRenderingParams1', com.STDMETHOD()), ] @@ -1139,7 +1154,7 @@ class IDWriteFactory2(IDWriteFactory1, com.pIUnknown): com.STDMETHOD()), ('TranslateColorGlyphRun', com.STDMETHOD()), - ('CreateCustomRenderingParams', + ('CreateCustomRenderingParams2', com.STDMETHOD()), ('CreateGlyphRunAnalysis', com.STDMETHOD()), @@ -1191,7 +1206,7 @@ class IDWriteFactory3(IDWriteFactory2, com.pIUnknown): _methods_ = [ ('CreateGlyphRunAnalysis', com.STDMETHOD()), - ('CreateCustomRenderingParams', + ('CreateCustomRenderingParams3', com.STDMETHOD()), ('CreateFontFaceReference', com.STDMETHOD()), @@ -1207,7 +1222,7 @@ class IDWriteFactory3(IDWriteFactory2, com.pIUnknown): com.STDMETHOD()), ('GetFontDownloadQueue', com.STDMETHOD()), - #('GetSystemFontSet', + # ('GetSystemFontSet', # com.STDMETHOD()), ] @@ -1220,10 +1235,12 @@ class IDWriteColorGlyphRunEnumerator1(com.pIUnknown): com.STDMETHOD()), ] + class IDWriteFactory4(IDWriteFactory3, com.pIUnknown): _methods_ = [ ('TranslateColorGlyphRun4', # Renamed to prevent clash from previous factories. - com.STDMETHOD(D2D_POINT_2F, DWRITE_GLYPH_RUN, c_void_p, DWRITE_GLYPH_IMAGE_FORMATS, DWRITE_MEASURING_MODE, c_void_p, UINT32, POINTER(IDWriteColorGlyphRunEnumerator1))), + com.STDMETHOD(D2D_POINT_2F, DWRITE_GLYPH_RUN, c_void_p, DWRITE_GLYPH_IMAGE_FORMATS, DWRITE_MEASURING_MODE, + c_void_p, UINT32, POINTER(IDWriteColorGlyphRunEnumerator1))), ('ComputeGlyphOrigins_', com.STDMETHOD()), ('ComputeGlyphOrigins', @@ -1259,7 +1276,6 @@ class IDWriteFactory5(IDWriteFactory4, IDWriteFactory3, IDWriteFactory2, IDWrite ] - DWriteCreateFactory = dwrite_lib.DWriteCreateFactory DWriteCreateFactory.restype = HRESULT DWriteCreateFactory.argtypes = [DWRITE_FACTORY_TYPE, com.REFIID, POINTER(com.pIUnknown)] @@ -1424,7 +1440,7 @@ class ID2D1RenderTarget(ID2D1Resource, com.pIUnknown): ('GetTransform', com.STDMETHOD()), ('SetAntialiasMode', - com.STDMETHOD()), + com.METHOD(c_void, D2D1_TEXT_ANTIALIAS_MODE)), ('GetAntialiasMode', com.STDMETHOD()), ('SetTextAntialiasMode', @@ -1432,7 +1448,7 @@ class ID2D1RenderTarget(ID2D1Resource, com.pIUnknown): ('GetTextAntialiasMode', com.STDMETHOD()), ('SetTextRenderingParams', - com.STDMETHOD()), + com.STDMETHOD(IDWriteRenderingParams)), ('GetTextRenderingParams', com.STDMETHOD()), ('SetTags', @@ -1444,7 +1460,7 @@ class ID2D1RenderTarget(ID2D1Resource, com.pIUnknown): ('PopLayer', com.STDMETHOD()), ('Flush', - com.STDMETHOD()), + com.STDMETHOD(c_void_p, c_void_p)), ('SaveDrawingState', com.STDMETHOD()), ('RestoreDrawingState', @@ -1535,6 +1551,7 @@ if not wic_decoder: class DirectWriteGlyphRenderer(base.GlyphRenderer): antialias_mode = D2D1_TEXT_ANTIALIAS_MODE_DEFAULT draw_options = D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT + measuring_mode = DWRITE_MEASURING_MODE_NATURAL def __init__(self, font): self._render_target = None @@ -1679,7 +1696,6 @@ class DirectWriteGlyphRenderer(base.GlyphRenderer): font_face.GetDesignGlyphMetrics(indices, count, glyph_metrics, False) metrics_out = [] - i = 0 for metric in glyph_metrics: glyph_width = (metric.advanceWidth - metric.leftSideBearing - metric.rightSideBearing) @@ -1696,7 +1712,6 @@ class DirectWriteGlyphRenderer(base.GlyphRenderer): advance_width = metric.advanceWidth metrics_out.append((glyph_width, glyph_height, lsb, advance_width, bsb)) - i += 1 return metrics_out @@ -1721,7 +1736,7 @@ class DirectWriteGlyphRenderer(base.GlyphRenderer): run, None, DWRITE_GLYPH_IMAGE_FORMATS_ALL, - DWRITE_MEASURING_MODE_NATURAL, + self.measuring_mode, None, 0, enumerator) @@ -1735,7 +1750,7 @@ class DirectWriteGlyphRenderer(base.GlyphRenderer): def render_single_glyph(self, font_face, indice, advance, offset, metrics): """Renders a single glyph using D2D DrawGlyphRun""" - glyph_width, glyph_height, lsb, font_advance, bsb = metrics # We use a shaped advance instead of the fonts. + glyph_width, glyph_height, glyph_lsb, glyph_advance, glyph_bsb = metrics # We use a shaped advance instead of the fonts. # Slicing an array turns it into a python object. Maybe a better way to keep it a ctypes value? new_indice = (UINT16 * 1)(indice) @@ -1748,19 +1763,24 @@ class DirectWriteGlyphRenderer(base.GlyphRenderer): new_advance, # advance, pointer(offset), # offset, False, - False + 0 ) # If it's colored, return to render it using layout. if self.draw_options & D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT and self.is_color_run(run): return None - render_width = int(math.ceil((glyph_width) * self.font.font_scale_ratio)) - render_offset_x = int(math.floor(abs(lsb * self.font.font_scale_ratio))) - if lsb < 0: - # Negative LSB: we shift the layout rect to the right - # Otherwise we will cut the left part of the glyph - render_offset_x = -(render_offset_x) + # Use the glyph's advance as a width as bitmap width. + # Some characters such as diacritics (̃) may have 0 advance width. In that case, just use glyph_width + if glyph_advance: + render_width = int(math.ceil(glyph_advance * self.font.font_scale_ratio)) + else: + render_width = int(math.ceil(glyph_width * self.font.font_scale_ratio)) + + render_offset_x = 0 + if glyph_lsb < 0: + # Negative LSB: we shift the offset, otherwise the glyph will be cut off. + render_offset_x = glyph_lsb * self.font.font_scale_ratio # Create new bitmap. # TODO: We can probably adjust bitmap/baseline to reduce the whitespace and save a lot of texture space. @@ -1769,11 +1789,9 @@ class DirectWriteGlyphRenderer(base.GlyphRenderer): int(math.ceil(self.font.max_glyph_height))) # Glyphs are drawn at the baseline, and with LSB, so we need to offset it based on top left position. - # Offsets are actually based on pixels somehow??? baseline_offset = D2D_POINT_2F(-render_offset_x - offset.advanceOffset, self.font.ascent + offset.ascenderOffset) - self._render_target.BeginDraw() self._render_target.Clear(transparent) @@ -1781,17 +1799,17 @@ class DirectWriteGlyphRenderer(base.GlyphRenderer): self._render_target.DrawGlyphRun(baseline_offset, run, self._brush, - DWRITE_MEASURING_MODE_NATURAL) + self.measuring_mode) self._render_target.EndDraw(None, None) image = wic_decoder.get_image(self._bitmap) glyph = self.font.create_glyph(image) - glyph.set_bearings(self.font.descent, render_offset_x, - advance * self.font.font_scale_ratio, - offset.advanceOffset * self.font.font_scale_ratio, - offset.ascenderOffset * self.font.font_scale_ratio) + glyph.set_bearings(-self.font.descent, render_offset_x, + advance, + offset.advanceOffset, + offset.ascenderOffset) return glyph @@ -1829,7 +1847,17 @@ class DirectWriteGlyphRenderer(base.GlyphRenderer): image = wic_decoder.get_image(self._bitmap) glyph = self.font.create_glyph(image) - glyph.set_bearings(self.font.descent, 0, int(math.ceil(layout_metrics.width))) + glyph.set_bearings(-self.font.descent, 0, int(math.ceil(layout_metrics.width))) + return glyph + + def create_zero_glyph(self): + """Zero glyph is a 1x1 image that has a -1 advance. This is to fill in for ligature substitutions since + font system requires 1 glyph per character in a string.""" + self._create_bitmap(1, 1) + image = wic_decoder.get_image(self._bitmap) + + glyph = self.font.create_glyph(image) + glyph.set_bearings(-self.font.descent, 0, -1) return glyph def _create_bitmap(self, width, height): @@ -1878,6 +1906,7 @@ class Win32DirectWriteFont(base.Font): _glyph_renderer = None _empty_glyph = None + _zero_glyph = None glyph_renderer_class = DirectWriteGlyphRenderer texture_internalformat = pyglet.gl.GL_RGBA @@ -1973,10 +2002,10 @@ class Win32DirectWriteFont(base.Font): self.font_scale_ratio = (self._real_size / self._font_metrics.designUnitsPerEm) - self.ascent = self._font_metrics.ascent * self.font_scale_ratio - self.descent = self._font_metrics.descent * self.font_scale_ratio - + self.ascent = math.ceil(self._font_metrics.ascent * self.font_scale_ratio) + self.descent = -round(self._font_metrics.descent * self.font_scale_ratio) self.max_glyph_height = (self._font_metrics.ascent + self._font_metrics.descent) * self.font_scale_ratio + self.line_gap = self._font_metrics.lineGap * self.font_scale_ratio self._fallback = None @@ -1986,7 +2015,6 @@ class Win32DirectWriteFont(base.Font): else: assert _debug_font("Windows 8.1+ is required for font fallback. Colored glyphs cannot be omitted.") - @property def name(self): return self._name @@ -2015,21 +2043,19 @@ class Win32DirectWriteFont(base.Font): new_glyph.set_bearings( glyph.baseline, glyph.lsb, - advance * self.font_scale_ratio, - offset.advanceOffset * self.font_scale_ratio, - offset.ascenderOffset * self.font_scale_ratio + advance, + offset.advanceOffset, + offset.ascenderOffset ) return new_glyph def _render_layout_glyph(self, text_buffer, i, clusters, check_color=True): - formatted_clusters = clusters[:] - # Some glyphs can be more than 1 char. We use the clusters to determine how many of an index exist. - text_length = formatted_clusters.count(i) + text_length = clusters.count(i) # Amount of glyphs don't always match 1:1 with text as some can be substituted or omitted. Get # actual text buffer index. - text_index = formatted_clusters.index(i) + text_index = clusters.index(i) # Get actual text based on the index and length. actual_text = text_buffer[text_index:text_index + text_length] @@ -2125,25 +2151,48 @@ class Win32DirectWriteFont(base.Font): if not self._glyph_renderer: self._glyph_renderer = self.glyph_renderer_class(self) self._empty_glyph = self._glyph_renderer.render_using_layout(" ") + self._zero_glyph = self._glyph_renderer.create_zero_glyph() - text_buffer, actual_count, indices, advances, offsets, clusters = self._glyph_renderer.get_string_info(text, self.font_face) + text_buffer, actual_count, indices, advances, offsets, clusters = self._glyph_renderer.get_string_info(text, + self.font_face) metrics = self._glyph_renderer.get_glyph_metrics(self.font_face, indices, actual_count) + formatted_clusters = list(clusters) + + # Convert to real sizes. + for i in range(actual_count): + advances[i] *= self.font_scale_ratio + + for i in range(actual_count): + offsets[i].advanceOffset *= self.font_scale_ratio + offsets[i].ascenderOffset *= self.font_scale_ratio + glyphs = [] + + # Pyglet expects 1 glyph for every string. However, ligatures can combine 1 or more glyphs, leading + # to issues with multilines producing wrong output. + substitutions = {} + for idx in clusters: + ct = formatted_clusters.count(idx) + if ct > 1: + substitutions[idx] = ct-1 + for i in range(actual_count): indice = indices[i] + if indice == 0: # If an indice is 0, it will return no glyph. In this case we attempt to render leveraging # the built in text layout from MS. Which depending on version can use fallback fonts and other tricks # to possibly get something of use. - glyph = self._render_layout_glyph(text_buffer, i, clusters) + glyph = self._render_layout_glyph(text_buffer, i, formatted_clusters) glyphs.append(glyph) else: + advance_key = (indice, advances[i], offsets[i].advanceOffset, offsets[i].ascenderOffset) + # Glyphs can vary depending on shaping. We will cache it by indice, advance, and offset. # Possible to just cache without offset and set them each time. This may be faster? if indice in self.glyphs: - advance_key = (indice, advances[i], offsets[i].advanceOffset, offsets[i].ascenderOffset) if advance_key in self._advance_cache: glyph = self._advance_cache[advance_key] else: @@ -2153,14 +2202,18 @@ class Win32DirectWriteFont(base.Font): glyph = self._glyph_renderer.render_single_glyph(self.font_face, indice, advances[i], offsets[i], metrics[i]) if glyph is None: # Will only return None if a color glyph is found. Use DW to render it directly. - glyph = self._render_layout_glyph(text_buffer, i, clusters, check_color=False) + glyph = self._render_layout_glyph(text_buffer, i, formatted_clusters, check_color=False) glyph.colored = True self.glyphs[indice] = glyph - self._advance_cache[(indice, advances[i], offsets[i].advanceOffset, offsets[i].ascenderOffset)] = glyph + self._advance_cache[advance_key] = glyph glyphs.append(glyph) + if i in substitutions: + for _ in range(substitutions[i]): + glyphs.append(self._zero_glyph) + return glyphs def create_text_layout(self, text): @@ -2367,4 +2420,4 @@ no_offset = D2D_POINT_2F(0, 0) # If we are not shaping, monkeypatch to no shape function. if pyglet.options["win32_disable_shaping"]: - Win32DirectWriteFont.get_glyphs = Win32DirectWriteFont.get_glyphs_no_shape \ No newline at end of file + Win32DirectWriteFont.get_glyphs = Win32DirectWriteFont.get_glyphs_no_shape diff --git a/libs/pyglet/font/freetype.py b/libs/pyglet/font/freetype.py index 9983db9..7e0bbde 100644 --- a/libs/pyglet/font/freetype.py +++ b/libs/pyglet/font/freetype.py @@ -120,7 +120,18 @@ class FreeTypeGlyphRenderer(base.GlyphRenderer): 'A', self._data, abs(self._pitch)) - glyph = self.font.create_glyph(img) + + # HACK: Get text working in GLES until image data can be converted properly + # GLES don't support coversion during pixel transfer so we have to + # force specify the glyph format to be GL_ALPHA. This format is not + # supported in 3.3+ core, but are present in ES because of pixel transfer + # limitations. + if pyglet.gl.current_context.get_info().get_opengl_api() == "gles": + GL_ALPHA = 0x1906 + glyph = self.font.create_glyph(img, fmt=GL_ALPHA) + else: + glyph = self.font.create_glyph(img) + glyph.set_bearings(self._baseline, self._lsb, self._advance_x) if self._pitch > 0: t = list(glyph.tex_coords) diff --git a/libs/pyglet/input/base.py b/libs/pyglet/input/base.py index 4b10f80..c5d2182 100644 --- a/libs/pyglet/input/base.py +++ b/libs/pyglet/input/base.py @@ -824,14 +824,14 @@ class Controller(EventDispatcher): # Input Event types: - def on_stick_motion(self, controller, axis, xvalue, yvalue): + def on_stick_motion(self, controller, stick, xvalue, yvalue): """The value of a controller analogue stick changed. :Parameters: `controller` : `Controller` The controller whose analogue stick changed. - `axis` : string - The name of the axis that changed. + `stick` : string + The name of the stick that changed. `xvalue` : float The current x axis value, normalized to [-1, 1]. `yvalue` : float diff --git a/libs/pyglet/input/controller.py b/libs/pyglet/input/controller.py index 5d2c93d..3034afe 100644 --- a/libs/pyglet/input/controller.py +++ b/libs/pyglet/input/controller.py @@ -55,12 +55,13 @@ To query which GameControllers are available, call :py:func:`get_controllers`. .. versionadded:: 2.0 """ -import os +import os as _os +import warnings as _warnings from .controller_db import mapping_list -_env_config = os.environ.get('SDL_GAMECONTROLLERCONFIG') +_env_config = _os.environ.get('SDL_GAMECONTROLLERCONFIG') if _env_config: # insert at the front of the list mapping_list.insert(0, _env_config) @@ -101,12 +102,11 @@ def _parse_mapping(mapping_string): if ':' not in item: continue - key, relation_string = item.split(':') + key, relation_string, *etc = item.split(':') if key not in valid_keys: continue - inverted = False # Look for specific flags to signify inverted axis: if "+" in relation_string: relation_string = relation_string.strip('+') @@ -114,9 +114,11 @@ def _parse_mapping(mapping_string): elif "-" in relation_string: relation_string = relation_string.strip('-') inverted = True - if "~" in relation_string: + elif "~" in relation_string: relation_string = relation_string.strip('~') inverted = True + else: + inverted = False # All relations will be one of (Button, Axis, or Hat). if relation_string.startswith("b"): # Button @@ -141,7 +143,11 @@ def get_mapping(guid): """ for mapping in mapping_list: if mapping.startswith(guid): - return _parse_mapping(mapping) + try: + return _parse_mapping(mapping) + except ValueError: + _warnings.warn(f"Unable to parse Controller mapping: {mapping}") + continue def add_mappings_from_file(filename) -> None: diff --git a/libs/pyglet/input/controller_db.py b/libs/pyglet/input/controller_db.py index 34b03ee..dbc09a0 100644 --- a/libs/pyglet/input/controller_db.py +++ b/libs/pyglet/input/controller_db.py @@ -2,7 +2,7 @@ from pyglet import compat_platform # This file is automatically generated by 'pyglet/tools/gen_controller_db.py' -# Generated on: Thu Jul 7 09:31:08 2022 +# Generated on: Wed Aug 3 10:51:56 2022 if compat_platform.startswith("linux"): mapping_list = [ @@ -27,6 +27,10 @@ if compat_platform.startswith("linux"): "05000000203800000900000000010000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "05000000c82d00002038000000010000,8BitDo NES30 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "05000000c82d00002038000000010000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"03000000c82d00000660000011010000,8BitDo Pro 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"03000000c82d00000660000011010000,8BitDo Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"05000000c82d00000660000000010000,8BitDo Pro 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"05000000c82d00000660000000010000,8BitDo Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "05000000c82d00000061000000010000,8BitDo SF30 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "05000000c82d00000061000000010000,8BitDo SF30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "05000000102800000900000000010000,8BitDo SFC30 Gamepad,a:b0,b:b1,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", @@ -105,6 +109,8 @@ if compat_platform.startswith("linux"): "030000000d0f00005001000009040000,HORI Fighting Commander OCTA,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,", "030000000d0f00008400000011010000,HORI Fighting Commander,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,", "030000000d0f00008500000010010000,HORI Fighting Commander,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,", +"030000000d0f00008800000011010000,HORI Fighting Stick mini 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,", +"030000000d0f00008700000011010000,HORI Fighting Stick mini 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,rightshoulder:b5,rightstick:b11,righttrigger:a4,start:b9,x:b0,y:b3,", "030000000d0f0000d800000072056800,HORI Real Arcade Pro S,a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,", "030000000d0f0000aa00000011010000,HORI Real Arcade Pro,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,", "030000000d0f00006e00000011010000,HORIPAD 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,", @@ -171,8 +177,26 @@ if compat_platform.startswith("linux"): "03000000790000004318000010010000,Nintendo GameCube Controller,a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "03000000790000004318000010010000,Nintendo GameCube Controller,a:b1,b:b0,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b2,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "030000007e0500003703000000016800,Nintendo GameCube Controller,a:b0,b:b2,dpdown:b6,dpleft:b4,dpright:b5,dpup:b7,lefttrigger:a4,leftx:a0,lefty:a1~,rightshoulder:b9,righttrigger:a5,rightx:a2,righty:a3~,start:b8,x:b1,y:b3,", +"050000004c69632050726f20436f6e00,Nintendo Switch Controller,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"050000004c69632050726f20436f6e00,Nintendo Switch Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"050000007e0500000620000001800000,Nintendo Switch Joy-Con (L),a:b15,b:b16,guide:b4,leftshoulder:b6,leftstick:b12,leftx:a1,lefty:a0~,rightshoulder:b8,start:b9,x:b17,y:b14,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"050000007e0500000620000001800000,Nintendo Switch Joy-Con (L),a:b16,b:b15,guide:b4,leftshoulder:b6,leftstick:b12,leftx:a1,lefty:a0~,rightshoulder:b8,start:b9,x:b14,y:b17,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"060000007e0500000620000000000000,Nintendo Switch Joy-Con (L/R),a:b1,b:b0,back:b9,dpdown:b15,dpleft:b16,dpright:b17,dpup:b14,leftshoulder:b5,leftstick:b12,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b13,righttrigger:b8,rightx:a2,righty:a3,start:b10,x:b2,y:b3,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"060000007e0500000620000000000000,Nintendo Switch Joy-Con (L/R),a:b0,b:b1,back:b9,dpdown:b15,dpleft:b16,dpright:b17,dpup:b14,leftshoulder:b5,leftstick:b12,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b13,righttrigger:b8,rightx:a2,righty:a3,start:b10,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"060000007e0500000820000000000000,Nintendo Switch Joy-Con (L/R),a:b1,b:b0,back:b9,dpdown:b15,dpleft:b16,dpright:b17,dpup:b14,guide:b11,leftshoulder:b5,leftstick:b12,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b13,righttrigger:b8,rightx:a2,righty:a3,start:b10,x:b2,y:b3,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"060000007e0500000820000000000000,Nintendo Switch Joy-Con (L/R),a:b0,b:b1,back:b9,dpdown:b15,dpleft:b16,dpright:b17,dpup:b14,guide:b11,leftshoulder:b5,leftstick:b12,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b13,righttrigger:b8,rightx:a2,righty:a3,start:b10,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"050000007e0500000720000001800000,Nintendo Switch Joy-Con (R),a:b2,b:b1,guide:b9,leftshoulder:b4,leftstick:b10,leftx:a1~,lefty:a0,rightshoulder:b6,start:b8,x:b3,y:b0,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"050000007e0500000720000001800000,Nintendo Switch Joy-Con (R),a:b1,b:b2,guide:b9,leftshoulder:b4,leftstick:b10,leftx:a1~,lefty:a0,rightshoulder:b6,start:b8,x:b0,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"03000000d620000013a7000011010000,Nintendo Switch PowerA Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"03000000d620000013a7000011010000,Nintendo Switch PowerA Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"03000000d620000011a7000011010000,Nintendo Switch PowerA Core Plus Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"03000000d620000011a7000011010000,Nintendo Switch PowerA Core Plus Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"030000007e0500000920000011810000,Nintendo Switch Pro Controller,a:b1,b:b0,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b5,leftstick:b12,lefttrigger:b7,leftx:a0,lefty:a1,misc1:b4,rightshoulder:b6,rightstick:b13,righttrigger:b8,rightx:a2,righty:a3,start:b10,x:b2,y:b3,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"030000007e0500000920000011810000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b5,leftstick:b12,lefttrigger:b7,leftx:a0,lefty:a1,misc1:b4,rightshoulder:b6,rightstick:b13,righttrigger:b8,rightx:a2,righty:a3,start:b10,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "050000007e0500000920000001000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "050000007e0500000920000001000000,Nintendo Switch Pro Controller,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"050000007e0500000920000001800000,Nintendo Switch Pro Controller,a:b1,b:b0,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b5,leftstick:b12,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b13,righttrigger:b8,rightx:a2,righty:a3,start:b10,x:b2,y:b3,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"050000007e0500000920000001800000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b5,leftstick:b12,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b13,righttrigger:b8,rightx:a2,righty:a3,start:b10,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "050000007e0500003003000001000000,Nintendo Wii Remote Pro Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,", "05000000010000000100000003000000,Nintendo Wiimote,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,", "030000000d0500000308000010010000,Nostromo n45 Dual Analog Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b12,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b10,x:b2,y:b3,", @@ -338,6 +362,8 @@ elif compat_platform.startswith("darwin"): "03000000022000000090000001000000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "03000000203800000900000000010000,8BitDo NES30 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "03000000203800000900000000010000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"03000000c82d00000660000000020000,8BitDo Pro 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"03000000c82d00000660000000020000,8BitDo Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "03000000102800000900000000000000,8BitDo SFC30 Gamepad,a:b0,b:b1,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "03000000102800000900000000000000,8BitDo SFC30 Gamepad,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "03000000c82d00001290000001000000,8BitDo SN30 Gamepad,a:b0,b:b1,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", @@ -375,6 +401,8 @@ elif compat_platform.startswith("darwin"): "03000000d11800000094000000010000,Google Stadia Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,", "030000000d0f00005f00000000000000,HORI Fighting Commander 4 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,", "030000000d0f00005e00000000000000,HORI Fighting Commander 4 PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,", +"030000000d0f00008800000000010000,HORI Fighting Stick mini 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,", +"030000000d0f00008700000000010000,HORI Fighting Stick mini 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,", "030000000d0f00004d00000000000000,HORI Gem Pad 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,", "030000000d0f0000aa00000072050000,HORI Real Arcade Pro,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,", "030000000d0f00006e00000000010000,HORIPAD 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,", @@ -490,6 +518,8 @@ elif compat_platform.startswith("win"): "03000000203800000900000000000000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "03000000c82d00002038000000000000,8BitDo NES30 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "03000000c82d00002038000000000000,8BitDo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"03000000c82d00000660000000000000,8BitDo Pro 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", +"03000000c82d00000660000000000000,8BitDo Pro 2,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "03000000c82d00000060000000000000,8BitDo SF30 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "03000000c82d00000060000000000000,8BitDo SF30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", "03000000c82d00000061000000000000,8BitDo SF30 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", @@ -554,8 +584,6 @@ elif compat_platform.startswith("win"): "03000000b80500000610000000000000,Elecom Gamepad,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b1,", "03000000852100000201000000000000,FF-GP1,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,", "030000000d0f00002700000000000000,FIGHTING STICK V3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,", -"030000000d0f00008700000000000000,Fighting Stick mini 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,", -"030000000d0f00008800000000000000,Fighting Stick mini 4,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b8,x:b0,y:b3,", "78696e70757403000000000000000000,Fightstick TES,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,start:b7,x:b2,y:b3,", "03000000151900004000000000000000,Flydigi Vader 2,a:b11,b:b10,back:b3,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b7,leftstick:b1,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b0,righttrigger:b4,rightx:a3,righty:a4,start:b2,x:b9,y:b8,", "03000000b40400001124000000000000,Flydigi Vader 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b12,lefttrigger:b8,leftx:a0,lefty:a1,paddle1:b4,paddle2:b5,paddle4:b17,rightshoulder:b7,rightstick:b13,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b2,y:b3,", @@ -582,6 +610,8 @@ elif compat_platform.startswith("win"): "03000000632500002605000000000000,HJD-X,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,", "030000000d0f00008400000000000000,HORI Fighting Commander,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,", "030000000d0f00008500000000000000,HORI Fighting Commander,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,", +"030000000d0f00008800000000000000,HORI Fighting Stick mini 4 (PS3),a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b8,x:b0,y:b3,", +"030000000d0f00008700000000000000,HORI Fighting Stick mini 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,", "030000000d0f00006e00000000000000,HORIPAD 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,", "030000000d0f00006600000000000000,HORIPAD 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,", "030000000d0f0000ee00000000000000,HORIPAD mini4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,", diff --git a/libs/pyglet/math.py b/libs/pyglet/math.py index ca098c4..2ba3fb8 100644 --- a/libs/pyglet/math.py +++ b/libs/pyglet/math.py @@ -46,13 +46,16 @@ for creating orthographic and perspective projection matrixes. the object is not updated in-place. """ +from __future__ import annotations + import math as _math import warnings as _warnings - +from collections.abc import Iterable, Iterator from operator import mul as _mul +from typing import NoReturn, TypeVar, cast, overload, Tuple -def clamp(num, min_val, max_val): +def clamp(num: float, min_val: float, max_val: float) -> float: return max(min(num, max_val), min_val) @@ -62,65 +65,73 @@ class Vec2: """A two-dimensional vector represented as an X Y coordinate pair. - :parameters: - `x` : int or float : + :parameters: + `x` : int or float : The X coordinate of the vector. `y` : int or float : The Y coordinate of the vector. """ - def __init__(self, x=0.0, y=0.0): + def __init__(self, x: float = 0.0, y: float = 0.0) -> None: self.x = x self.y = y - def __iter__(self): + def __iter__(self) -> Iterator[float]: yield self.x yield self.y - def __len__(self): + def __len__(self) -> int: return 2 + @overload + def __getitem__(self, item: int) -> float: + ... + + @overload + def __getitem__(self, item: slice) -> tuple[float, ...]: + ... + def __getitem__(self, item): return (self.x, self.y)[item] - def __add__(self, other): + def __add__(self, other: Vec2) -> Vec2: return Vec2(self.x + other.x, self.y + other.y) - def __sub__(self, other): + def __sub__(self, other: Vec2) -> Vec2: return Vec2(self.x - other.x, self.y - other.y) - def __mul__(self, other): + def __mul__(self, other: Vec2) -> Vec2: return Vec2(self.x * other.x, self.y * other.y) - def __truediv__(self, other): + def __truediv__(self, other: Vec2) -> Vec2: return Vec2(self.x / other.x, self.y / other.y) - def __abs__(self): + def __abs__(self) -> float: return _math.sqrt(self.x ** 2 + self.y ** 2) - def __neg__(self): + def __neg__(self) -> Vec2: return Vec2(-self.x, -self.y) - def __round__(self, ndigits=None): + def __round__(self, ndigits: int | None = None) -> Vec2: return Vec2(*(round(v, ndigits) for v in self)) - def __radd__(self, other): + def __radd__(self, other: Vec2 | int) -> Vec2: """Reverse add. Required for functionality with sum() """ if other == 0: return self else: - return self.__add__(other) + return self.__add__(cast(Vec2, other)) - def __eq__(self, other): - return self.x == other.x and self.y == other.y + def __eq__(self, other: object) -> bool: + return isinstance(other, Vec2) and self.x == other.x and self.y == other.y - def __ne__(self, other): - return self.x != other.x or self.y != other.y + def __ne__(self, other: object) -> bool: + return not isinstance(other, Vec2) or self.x != other.x or self.y != other.y @staticmethod - def from_polar(mag, angle): + def from_polar(mag: float, angle: float) -> Vec2: """Create a new vector from the given polar coordinates. :parameters: @@ -134,12 +145,12 @@ class Vec2: """ return Vec2(mag * _math.cos(angle), mag * _math.sin(angle)) - def from_magnitude(self, magnitude): + def from_magnitude(self, magnitude: float) -> Vec2: """Create a new Vector of the given magnitude by normalizing, then scaling the vector. The heading remains unchanged. - :parameters: - `magnitude` : int or float : + :parameters: + `magnitude` : int or float : The magnitude of the new vector. :returns: A new vector with the magnitude. @@ -147,13 +158,13 @@ class Vec2: """ return self.normalize().scale(magnitude) - def from_heading(self, heading): + def from_heading(self, heading: float) -> Vec2: """Create a new vector of the same magnitude with the given heading. I.e. Rotate the vector to the heading. - :parameters: + :parameters: `heading` : int or float : The angle of the new vector in radians. - + :returns: A new vector with the given heading. :rtype: Vec2 """ @@ -161,7 +172,7 @@ class Vec2: return Vec2(mag * _math.cos(heading), mag * _math.sin(heading)) @property - def heading(self): + def heading(self) -> float: """The angle of the vector in radians. :type: float @@ -169,7 +180,7 @@ class Vec2: return _math.atan2(self.y, self.x) @property - def mag(self): + def mag(self) -> float: """The magnitude, or length of the vector. The distance between the coordinates and the origin. Alias of abs(self). @@ -178,54 +189,54 @@ class Vec2: """ return self.__abs__() - def limit(self, maximum): + def limit(self, maximum: float) -> Vec2: """Limit the magnitude of the vector to the value used for the max parameter. - :parameters: + :parameters: `maximum` : int or float : The maximum magnitude for the vector. - + :returns: Either self or a new vector with the maximum magnitude. :rtype: Vec2 """ if self.x ** 2 + self.y ** 2 > maximum * maximum: return self.from_magnitude(maximum) return self - - def lerp(self, other, alpha): - """Create a new vector lineraly interpolated between this vector and another vector. - :parameters: + def lerp(self, other: Vec2, alpha: float) -> Vec2: + """Create a new Vec2 linearly interpolated between this vector and another Vec2. + + :parameters: `other` : Vec2 : - The vector to be linerly interpolated to. + The vector to linearly interpolate with. `alpha` : float or int : The amount of interpolation. - Some value between 0.0 (this vector) and 1.0 (other vector). + Some value between 0.0 (this vector) and 1.0 (other vector). 0.5 is halfway inbetween. - + :returns: A new interpolated vector. :rtype: Vec2 """ return Vec2(self.x + (alpha * (other.x - self.x)), self.y + (alpha * (other.y - self.y))) - def scale(self, value): + def scale(self, value: float) -> Vec2: """Multiply the vector by a scalar value. - :parameters: + :parameters: `value` : int or float : - The ammount to be scaled by + The value to scale the vector by. :returns: A new vector scaled by the value. :rtype: Vec2 """ return Vec2(self.x * value, self.y * value) - def rotate(self, angle): + def rotate(self, angle: float) -> Vec2: """Create a new Vector rotated by the angle. The magnitude remains unchanged. - :parameters: - `angle` : int or float : + :parameters: + `angle` : int or float : The angle to rotate by :returns: A new rotated vector of the same magnitude. @@ -233,21 +244,21 @@ class Vec2: """ mag = self.mag heading = self.heading - return Vec2(mag * _math.cos(heading + angle), mag * _math.sin(heading+angle)) + return Vec2(mag * _math.cos(heading + angle), mag * _math.sin(heading + angle)) - def distance(self, other): + def distance(self, other: Vec2) -> float: """Calculate the distance between this vector and another 2D vector. - + :parameters: `other` : Vec2 : - The other vector + The other vector :returns: The distance between the two vectors. :rtype: float """ return _math.sqrt(((other.x - self.x) ** 2) + ((other.y - self.y) ** 2)) - def normalize(self): + def normalize(self) -> Vec2: """Normalize the vector to have a magnitude of 1. i.e. make it a unit vector. :returns: A unit vector with the same heading. @@ -258,11 +269,11 @@ class Vec2: return Vec2(self.x / d, self.y / d) return self - def clamp(self, min_val, max_val): + def clamp(self, min_val: float, max_val: float) -> Vec2: """Restrict the value of the X and Y components of the vector to be within the given values. - :parameters: - `min_val` : int or float : + :parameters: + `min_val` : int or float : The minimum value `max_val` : int or float : The maximum value @@ -272,27 +283,29 @@ class Vec2: """ return Vec2(clamp(self.x, min_val, max_val), clamp(self.y, min_val, max_val)) - def dot(self, other): + def dot(self, other: Vec2) -> float: """Calculate the dot product of this vector and another 2D vector. - :parameters: + :parameters: `other` : Vec2 : The other vector. - + :returns: The dot product of the two vectors. :rtype: float """ return self.x * other.x + self.y * other.y - def __getattr__(self, attrs): + def __getattr__(self, attrs: str) -> Vec2 | Vec3 | Vec4: try: - # Allow swizzed getting of attrs - vec_class = {2: Vec2, 3: Vec3, 4: Vec4}.get(len(attrs)) + # Allow swizzled getting of attrs + vec_class = {2: Vec2, 3: Vec3, 4: Vec4}[len(attrs)] return vec_class(*(self['xy'.index(c)] for c in attrs)) except Exception: - raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attrs}'") + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{attrs}'" + ) from None - def __repr__(self): + def __repr__(self) -> str: return f"Vec2({self.x}, {self.y})" @@ -302,8 +315,8 @@ class Vec3: """A three-dimensional vector represented as X Y Z coordinates. - :parameters: - `x` : int or float : + :parameters: + `x` : int or float : The X coordinate of the vector. `y` : int or float : The Y coordinate of the vector. @@ -312,24 +325,32 @@ class Vec3: """ - def __init__(self, x=0.0, y=0.0, z=0.0): + def __init__(self, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> None: self.x = x self.y = y self.z = z - def __iter__(self): + def __iter__(self) -> Iterator[float]: yield self.x yield self.y yield self.z + @overload + def __getitem__(self, item: int) -> float: + ... + + @overload + def __getitem__(self, item: slice) -> tuple[float, ...]: + ... + def __getitem__(self, item): return (self.x, self.y, self.z)[item] - def __len__(self): + def __len__(self) -> int: return 3 @property - def mag(self): + def mag(self) -> float: """The magnitude, or length of the vector. The distance between the coordinates and the origin. Alias of abs(self). @@ -338,47 +359,47 @@ class Vec3: """ return self.__abs__() - def __add__(self, other): + def __add__(self, other: Vec3) -> Vec3: return Vec3(self.x + other.x, self.y + other.y, self.z + other.z) - def __sub__(self, other): + def __sub__(self, other: Vec3) -> Vec3: return Vec3(self.x - other.x, self.y - other.y, self.z - other.z) - def __mul__(self, other): + def __mul__(self, other: Vec3) -> Vec3: return Vec3(self.x * other.x, self.y * other.y, self.z * other.z) - def __truediv__(self, other): + def __truediv__(self, other: Vec3) -> Vec3: return Vec3(self.x / other.x, self.y / other.y, self.z / other.z) - def __abs__(self): + def __abs__(self) -> float: return _math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2) - def __neg__(self): + def __neg__(self) -> Vec3: return Vec3(-self.x, -self.y, -self.z) - def __round__(self, ndigits=None): + def __round__(self, ndigits: int | None = None) -> Vec3: return Vec3(*(round(v, ndigits) for v in self)) - def __radd__(self, other): + def __radd__(self, other: Vec3 | int) -> Vec3: """Reverse add. Required for functionality with sum() """ if other == 0: return self else: - return self.__add__(other) + return self.__add__(cast(Vec3, other)) - def __eq__(self, other): - return self.x == other.x and self.y == other.y and self.z == other.z + def __eq__(self, other: object) -> bool: + return isinstance(object, Vec3) and self.x == other.x and self.y == other.y and self.z == other.z - def __ne__(self, other): - return self.x != other.x or self.y != other.y or self.z != other.z + def __ne__(self, other: object) -> bool: + return not isinstance(object, Vec3) or self.x != other.x or self.y != other.y or self.z != other.z - def from_magnitude(self, magnitude): + def from_magnitude(self, magnitude: float) -> Vec3: """Create a new Vector of the given magnitude by normalizing, then scaling the vector. The rotation remains unchanged. - :parameters: - `magnitude` : int or float : + :parameters: + `magnitude` : int or float : The magnitude of the new vector. :returns: A new vector with the magnitude. @@ -386,27 +407,27 @@ class Vec3: """ return self.normalize().scale(magnitude) - def limit(self, maximum): + def limit(self, maximum: float) -> Vec3: """Limit the magnitude of the vector to the value used for the max parameter. - :parameters: + :parameters: `maximum` : int or float : The maximum magnitude for the vector. - + :returns: Either self or a new vector with the maximum magnitude. :rtype: Vec3 """ if self.x ** 2 + self.y ** 2 + self.z ** 2 > maximum * maximum * maximum: - return self.from_magnitude(max) + return self.from_magnitude(maximum) return self - def cross(self, other): + def cross(self, other: Vec3) -> Vec3: """Calculate the cross product of this vector and another 3D vector. - :parameters: + :parameters: `other` : Vec3 : The other vector. - + :returns: The cross product of the two vectors. :rtype: float """ @@ -414,7 +435,7 @@ class Vec3: (self.z * other.x) - (self.x * other.z), (self.x * other.y) - (self.y * other.x)) - def dot(self, other): + def dot(self, other: Vec3) -> float: """Calculate the dot product of this vector and another 3D vector. :parameters: @@ -426,17 +447,17 @@ class Vec3: """ return self.x * other.x + self.y * other.y + self.z * other.z - def lerp(self, other, alpha): - """Create a new vector lineraly interpolated between this vector and another vector. + def lerp(self, other: Vec3, alpha: float) -> Vec3: + """Create a new Vec3 linearly interpolated between this vector and another Vec3. - :parameters: + :parameters: `other` : Vec3 : - The vector to be linerly interpolated to. + The vector to linearly interpolate with. `alpha` : float or int : The amount of interpolation. - Some value between 0.0 (this vector) and 1.0 (other vector). + Some value between 0.0 (this vector) and 1.0 (other vector). 0.5 is halfway inbetween. - + :returns: A new interpolated vector. :rtype: Vec3 """ @@ -444,24 +465,24 @@ class Vec3: self.y + (alpha * (other.y - self.y)), self.z + (alpha * (other.z - self.z))) - def scale(self, value): + def scale(self, value: float) -> Vec3: """Multiply the vector by a scalar value. - :parameters: + :parameters: `value` : int or float : - The ammount to be scaled by + The value to scale the vector by. :returns: A new vector scaled by the value. :rtype: Vec3 """ return Vec3(self.x * value, self.y * value, self.z * value) - def distance(self, other): + def distance(self, other: Vec3) -> float: """Calculate the distance between this vector and another 3D vector. - + :parameters: `other` : Vec3 : - The other vector + The other vector :returns: The distance between the two vectors. :rtype: float @@ -470,7 +491,7 @@ class Vec3: ((other.y - self.y) ** 2) + ((other.z - self.z) ** 2)) - def normalize(self): + def normalize(self) -> Vec3: """Normalize the vector to have a magnitude of 1. i.e. make it a unit vector. :returns: A unit vector with the same rotation. @@ -481,11 +502,11 @@ class Vec3: return Vec3(self.x / d, self.y / d, self.z / d) return self - def clamp(self, min_val, max_val): + def clamp(self, min_val: float, max_val: float) -> Vec3: """Restrict the value of the X, Y and Z components of the vector to be within the given values. - :parameters: - `min_val` : int or float : + :parameters: + `min_val` : int or float : The minimum value `max_val` : int or float : The maximum value @@ -497,15 +518,17 @@ class Vec3: clamp(self.y, min_val, max_val), clamp(self.z, min_val, max_val)) - def __getattr__(self, attrs): + def __getattr__(self, attrs: str) -> Vec2 | Vec3 | Vec4: try: - # Allow swizzed getting of attrs - vec_class = {2: Vec2, 3: Vec3, 4: Vec4}.get(len(attrs)) + # Allow swizzled getting of attrs + vec_class = {2: Vec2, 3: Vec3, 4: Vec4}[len(attrs)] return vec_class(*(self['xyz'.index(c)] for c in attrs)) except Exception: - raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attrs}'") + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{attrs}'" + ) from None - def __repr__(self): + def __repr__(self) -> str: return f"Vec3({self.x}, {self.y}, {self.z})" @@ -515,8 +538,8 @@ class Vec4: """A four-dimensional vector represented as X Y Z W coordinates. - :parameters: - `x` : int or float : + :parameters: + `x` : int or float : The X coordinate of the vector. `y` : int or float : The Y coordinate of the vector. @@ -527,100 +550,144 @@ class Vec4: """ - def __init__(self, x=0.0, y=0.0, z=0.0, w=0.0): + def __init__(self, x: float = 0.0, y: float = 0.0, z: float = 0.0, w: float = 0.0) -> None: self.x = x self.y = y self.z = z self.w = w - def __iter__(self): + def __iter__(self) -> Iterator[float]: yield self.x yield self.y yield self.z yield self.w + @overload + def __getitem__(self, item: int) -> float: + ... + + @overload + def __getitem__(self, item: slice) -> tuple[float, ...]: + ... + def __getitem__(self, item): return (self.x, self.y, self.z, self.w)[item] - def __len__(self): + def __len__(self) -> int: return 4 - def __add__(self, other): + def __add__(self, other: Vec4) -> Vec4: return Vec4(self.x + other.x, self.y + other.y, self.z + other.z, self.w + other.w) - def __sub__(self, other): + def __sub__(self, other: Vec4) -> Vec4: return Vec4(self.x - other.x, self.y - other.y, self.z - other.z, self.w - other.w) - def __mul__(self, other): + def __mul__(self, other: Vec4) -> Vec4: return Vec4(self.x * other.x, self.y * other.y, self.z * other.z, self.w * other.w) - def __truediv__(self, other): + def __truediv__(self, other: Vec4) -> Vec4: return Vec4(self.x / other.x, self.y / other.y, self.z / other.z, self.w / other.w) - def __abs__(self): + def __abs__(self) -> float: return _math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2 + self.w ** 2) - def __neg__(self): + def __neg__(self) -> Vec4: return Vec4(-self.x, -self.y, -self.z, -self.w) - def __round__(self, ndigits=None): + def __round__(self, ndigits: int | None = None) -> Vec4: return Vec4(*(round(v, ndigits) for v in self)) - def __radd__(self, other): + def __radd__(self, other: Vec4 | int) -> Vec4: if other == 0: return self else: - return self.__add__(other) + return self.__add__(cast(Vec4, other)) - def __eq__(self, other): - return self.x == other.x and self.y == other.y and self.z == other.z and self.w == other.w + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, Vec4) + and self.x == other.x + and self.y == other.y + and self.z == other.z + and self.w == other.w + ) - def __ne__(self, other): - return self.x != other.x or self.y != other.y or self.z != other.z or self.w != other.w + def __ne__(self, other: object) -> bool: + return ( + not isinstance(other, Vec4) + or self.x != other.x + or self.y != other.y + or self.z != other.z + or self.w != other.w + ) - def lerp(self, other, alpha): + def lerp(self, other: Vec4, alpha: float) -> Vec4: + """Create a new Vec4 linearly interpolated between this one and another Vec4. + + :parameters: + `other` : Vec4 : + The vector to linearly interpolate with. + `alpha` : float or int : + The amount of interpolation. + Some value between 0.0 (this vector) and 1.0 (other vector). + 0.5 is halfway inbetween. + + :returns: A new interpolated vector. + :rtype: Vec4 + """ return Vec4(self.x + (alpha * (other.x - self.x)), self.y + (alpha * (other.y - self.y)), self.z + (alpha * (other.z - self.z)), self.w + (alpha * (other.w - self.w))) - def scale(self, value): + def scale(self, value: float) -> Vec4: + """Multiply the vector by a scalar value. + + :parameters: + `value` : int or float : + The value to scale the vector by. + + :returns: A new vector scaled by the value. + :rtype: Vec4 + """ return Vec4(self.x * value, self.y * value, self.z * value, self.w * value) - def distance(self, other): + def distance(self, other: Vec4) -> float: return _math.sqrt(((other.x - self.x) ** 2) + ((other.y - self.y) ** 2) + ((other.z - self.z) ** 2) + ((other.w - self.w) ** 2)) - def normalize(self): + def normalize(self) -> Vec4: d = self.__abs__() if d: return Vec4(self.x / d, self.y / d, self.z / d, self.w / d) return self - def clamp(self, min_val, max_val): + def clamp(self, min_val: float, max_val: float) -> Vec4: return Vec4(clamp(self.x, min_val, max_val), clamp(self.y, min_val, max_val), clamp(self.z, min_val, max_val), clamp(self.w, min_val, max_val)) - def dot(self, other): + def dot(self, other: Vec4) -> float: return self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w - def __getattr__(self, attrs): + def __getattr__(self, attrs: str) -> Vec2 | Vec3 | Vec4: try: - # Allow swizzed getting of attrs - vec_class = {2: Vec2, 3: Vec3, 4: Vec4}.get(len(attrs)) + # Allow swizzled getting of attrs + vec_class = {2: Vec2, 3: Vec3, 4: Vec4}[len(attrs)] return vec_class(*(self['xyzw'.index(c)] for c in attrs)) except Exception: - raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attrs}'") + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{attrs}'" + ) from None - def __repr__(self): + def __repr__(self) -> str: return f"Vec4({self.x}, {self.y}, {self.z}, {self.w})" -class Mat3(tuple): +class Mat3(Tuple[float, float, float, float, float, float, float, float, float]): """A 3x3 Matrix class `Mat3` is an immutable 3x3 Matrix, including most common @@ -628,7 +695,9 @@ class Mat3(tuple): the "@" operator. """ - def __new__(cls, values=None) -> 'Mat3': + def __new__( + cls, values: Iterable[float] = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0) + ) -> Mat3: """Create a 3x3 Matrix A Mat3 can be created with a list or tuple of 9 values. @@ -640,49 +709,56 @@ class Mat3(tuple): `values` : tuple of float or int A tuple or list containing 9 floats or ints. """ - assert values is None or len(values) == 9, "A 3x3 Matrix requires 9 values" - return super().__new__(Mat3, values or (1.0, 0.0, 0.0, - 0.0, 1.0, 0.0, - 0.0, 0.0, 1.0)) + new = super().__new__(Mat3, values) + assert len(new) == 9, "A 3x3 Matrix requires 9 values" + return new - def scale(self, sx: float, sy: float): - return self @ (1.0 / sx, 0.0, 0.0, 0.0, 1.0 / sy, 0.0, 0.0, 0.0, 1.0) + def scale(self, sx: float, sy: float) -> Mat3: + return self @ Mat3((1.0 / sx, 0.0, 0.0, 0.0, 1.0 / sy, 0.0, 0.0, 0.0, 1.0)) - def translate(self, tx: float, ty: float): - return self @ (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, -tx, ty, 1.0) + def translate(self, tx: float, ty: float) -> Mat3: + return self @ Mat3((1.0, 0.0, 0.0, 0.0, 1.0, 0.0, -tx, ty, 1.0)) - def rotate(self, phi: float): + def rotate(self, phi: float) -> Mat3: s = _math.sin(_math.radians(phi)) c = _math.cos(_math.radians(phi)) - return self @ (c, s, 0.0, -s, c, 0.0, 0.0, 0.0, 1.0) + return self @ Mat3((c, s, 0.0, -s, c, 0.0, 0.0, 0.0, 1.0)) - def shear(self, sx: float, sy: float): - return self @ (1.0, sy, 0.0, sx, 1.0, 0.0, 0.0, 0.0, 1.0) + def shear(self, sx: float, sy: float) -> Mat3: + return self @ Mat3((1.0, sy, 0.0, sx, 1.0, 0.0, 0.0, 0.0, 1.0)) - def __add__(self, other) -> 'Mat3': - assert len(other) == 9, "Can only add to other Mat3 types" - return Mat3(tuple(s + o for s, o in zip(self, other))) + def __add__(self, other: Mat3) -> Mat3: + if not isinstance(other, Mat3): + raise TypeError("Can only add to other Mat3 types") + return Mat3(s + o for s, o in zip(self, other)) - def __sub__(self, other) -> 'Mat3': - assert len(other) == 9, "Can only subtract from other Mat3 types" - return Mat3(tuple(s - o for s, o in zip(self, other))) + def __sub__(self, other: Mat3) -> Mat3: + if not isinstance(other, Mat3): + raise TypeError("Can only subtract from other Mat3 types") + return Mat3(s - o for s, o in zip(self, other)) - def __pos__(self): + def __pos__(self) -> Mat3: return self - def __neg__(self) -> 'Mat3': - return Mat3(tuple(-v for v in self)) + def __neg__(self) -> Mat3: + return Mat3(-v for v in self) - def __round__(self, ndigits=None) -> 'Mat3': - return Mat3(tuple(round(v, ndigits) for v in self)) + def __round__(self, ndigits: int | None = None) -> Mat3: + return Mat3(round(v, ndigits) for v in self) - def __mul__(self, other): + def __mul__(self, other: object) -> NoReturn: raise NotImplementedError("Please use the @ operator for Matrix multiplication.") - def __matmul__(self, other) -> 'Mat3' or 'Vec3': - assert len(other) in (3, 9), "Can only multiply with Mat3 or Vec3 types" + @overload + def __matmul__(self, other: Vec3) -> Vec3: + ... - if type(other) is Vec3: + @overload + def __matmul__(self, other: Mat3) -> Mat3: + ... + + def __matmul__(self, other): + if isinstance(other, Vec3): # Columns: c0 = self[0::3] c1 = self[1::3] @@ -691,6 +767,9 @@ class Mat3(tuple): sum(map(_mul, c1, other)), sum(map(_mul, c2, other))) + if not isinstance(other, Mat3): + raise TypeError("Can only multiply with Mat3 or Vec3 types") + # Rows: r0 = self[0:3] r1 = self[3:6] @@ -717,7 +796,17 @@ class Mat3(tuple): return f"{self.__class__.__name__}{self[0:3]}\n {self[3:6]}\n {self[6:9]}" -class Mat4(tuple): +Mat4T = TypeVar("Mat4T", bound="Mat4") + + +class Mat4( + Tuple[ + float, float, float, float, + float, float, float, float, + float, float, float, float, + float, float, float, float, + ] +): """A 4x4 Matrix class `Mat4` is an immutable 4x4 Matrix, including most common @@ -727,7 +816,15 @@ class Mat4(tuple): and perspective projections matrixes. """ - def __new__(cls, values=None) -> 'Mat4': + def __new__( + cls, + values: Iterable[float] = ( + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0, + ), + ) -> Mat4: """Create a 4x4 Matrix A Matrix can be created with a list or tuple of 16 values. @@ -739,14 +836,21 @@ class Mat4(tuple): `values` : tuple of float or int A tuple or list containing 16 floats or ints. """ - assert values is None or len(values) == 16, "A 4x4 Matrix requires 16 values" - return super().__new__(Mat4, values or (1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0)) + + new = super().__new__(Mat4, values) + assert len(new) == 16, "A 4x4 Matrix requires 16 values" + return new @classmethod - def orthogonal_projection(cls, left, right, bottom, top, z_near, z_far) -> 'Mat4': + def orthogonal_projection( + cls: type[Mat4T], + left: float, + right: float, + bottom: float, + top: float, + z_near: float, + z_far: float + ) -> Mat4T: """Create a Mat4 orthographic projection matrix.""" width = right - left height = top - bottom @@ -766,7 +870,13 @@ class Mat4(tuple): tx, ty, tz, 1.0)) @classmethod - def perspective_projection(cls, aspect, z_near, z_far, fov=60) -> 'Mat4': + def perspective_projection( + cls: type[Mat4T], + aspect: float, + z_near: float, + z_far: float, + fov: float = 60 + ) -> Mat4T: """ Create a Mat4 perspective projection matrix. @@ -796,7 +906,7 @@ class Mat4(tuple): 0, 0, qn, 0)) @classmethod - def from_translation(cls, vector: Vec3) -> 'Mat4': + def from_translation(cls: type[Mat4T], vector: Vec3) -> Mat4T: """Create a translation matrix from a Vec3. :Parameters: @@ -809,7 +919,7 @@ class Mat4(tuple): vector[0], vector[1], vector[2], 1.0)) @classmethod - def from_rotation(cls, angle: float, vector: Vec3) -> 'Mat4': + def from_rotation(cls, angle: float, vector: Vec3) -> Mat4: """Create a rotation matrix from an angle and Vec3. :Parameters: @@ -821,10 +931,10 @@ class Mat4(tuple): return cls().rotate(angle, vector) @classmethod - def look_at_direction(cls, direction: Vec3, up: Vec3) -> 'Mat4': + def look_at_direction(cls: type[Mat4T], direction: Vec3, up: Vec3) -> Mat4T: vec_z = direction.normalize() - vec_x = direction.cross_product(up).normalize() - vec_y = direction.cross_product(vec_z).normalize() + vec_x = direction.cross(up).normalize() + vec_y = direction.cross(vec_z).normalize() return cls((vec_x.x, vec_y.x, vec_z.x, 0.0, vec_x.y, vec_y.y, vec_z.y, 0.0, @@ -832,21 +942,21 @@ class Mat4(tuple): 0.0, 0.0, 0.0, 1.0)) @classmethod - def look_at(cls, position: Vec3, target: Vec3, up: Vec3) -> 'Mat4': + def look_at(cls, position: Vec3, target: Vec3, up: Vec3) -> Mat4: direction = target - position direction_mat4 = cls.look_at_direction(direction, up) - position_mat4 = cls.from_translation(position.negate()) + position_mat4 = cls.from_translation(-position) return direction_mat4 @ position_mat4 - def row(self, index: int): + def row(self, index: int) -> tuple: """Get a specific row as a tuple.""" - return self[index*4:index*4+4] + return self[index * 4 : index * 4 + 4] - def column(self, index: int): + def column(self, index: int) -> tuple: """Get a specific column as a tuple.""" return self[index::4] - def scale(self, vector: Vec3) -> 'Mat4': + def scale(self, vector: Vec3) -> Mat4: """Get a scale Matrix on x, y, or z axis.""" temp = list(self) temp[0] *= vector[0] @@ -854,13 +964,14 @@ class Mat4(tuple): temp[10] *= vector[2] return Mat4(temp) - def translate(self, vector: Vec3) -> 'Mat4': + def translate(self, vector: Vec3) -> Mat4: """Get a translation Matrix along x, y, and z axis.""" - return Mat4(self) @ Mat4((1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, *vector, 1)) + return self @ Mat4((1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, *vector, 1)) - def rotate(self, angle: float, vector: Vec3) -> 'Mat4': + def rotate(self, angle: float, vector: Vec3) -> Mat4: """Get a rotation Matrix on x, y, or z axis.""" - assert all(abs(n) <= 1 for n in vector), "vector must be normalized (<=1)" + if not all(abs(n) <= 1 for n in vector): + raise ValueError("vector must be normalized (<=1)") x, y, z = vector c = _math.cos(angle) s = _math.sin(angle) @@ -884,25 +995,27 @@ class Mat4(tuple): return Mat4(self) @ Mat4((ra, rb, rc, 0, re, rf, rg, 0, ri, rj, rk, 0, 0, 0, 0, 1)) - def transpose(self) -> 'Mat4': + def transpose(self) -> Mat4: """Get a transpose of this Matrix.""" return Mat4(self[0::4] + self[1::4] + self[2::4] + self[3::4]) - def __add__(self, other) -> 'Mat4': - assert len(other) == 16, "Can only add to other Mat4 types" - return Mat4(tuple(s + o for s, o in zip(self, other))) + def __add__(self, other: Mat4) -> Mat4: + if not isinstance(other, Mat4): + raise TypeError("Can only add to other Mat4 types") + return Mat4(s + o for s, o in zip(self, other)) - def __sub__(self, other) -> 'Mat4': - assert len(other) == 16, "Can only subtract from other Mat4 types" - return Mat4(tuple(s - o for s, o in zip(self, other))) + def __sub__(self, other: Mat4) -> Mat4: + if not isinstance(other, Mat4): + raise TypeError("Can only subtract from other Mat4 types") + return Mat4(s - o for s, o in zip(self, other)) - def __pos__(self): + def __pos__(self) -> Mat4: return self - def __neg__(self) -> 'Mat4': - return Mat4(tuple(-v for v in self)) + def __neg__(self) -> Mat4: + return Mat4(-v for v in self) - def __invert__(self) -> 'Mat4': + def __invert__(self) -> Mat4: a = self[10] * self[15] - self[11] * self[14] b = self[9] * self[15] - self[11] * self[13] c = self[9] * self[14] - self[10] * self[13] @@ -951,16 +1064,22 @@ class Mat4(tuple): ndet * (self[0] * i - self[1] * n + self[2] * q), pdet * (self[0] * l - self[1] * p + self[2] * r))) - def __round__(self, ndigits=None) -> 'Mat4': - return Mat4(tuple(round(v, ndigits) for v in self)) + def __round__(self, ndigits: int | None = None) -> Mat4: + return Mat4(round(v, ndigits) for v in self) - def __mul__(self, other): + def __mul__(self, other: int) -> NoReturn: raise NotImplementedError("Please use the @ operator for Matrix multiplication.") - def __matmul__(self, other) -> 'Mat4' or 'Vec4': - assert len(other) in (4, 16), "Can only multiply with Mat4 or Vec4 types" + @overload + def __matmul__(self, other: Vec4) -> Vec4: + ... - if type(other) is Vec4: + @overload + def __matmul__(self, other: Mat4) -> Mat4: + ... + + def __matmul__(self, other): + if isinstance(other, Vec4): # Columns: c0 = self[0::4] c1 = self[1::4] @@ -971,6 +1090,8 @@ class Mat4(tuple): sum(map(_mul, c2, other)), sum(map(_mul, c3, other))) + if not isinstance(other, Mat4): + raise TypeError("Can only multiply with Mat4 or Vec4 types") # Rows: r0 = self[0:4] r1 = self[4:8] diff --git a/libs/pyglet/media/codecs/base.py b/libs/pyglet/media/codecs/base.py index e60f63b..9474351 100644 --- a/libs/pyglet/media/codecs/base.py +++ b/libs/pyglet/media/codecs/base.py @@ -123,7 +123,7 @@ class AudioData: length (int): Size of sample data, in bytes. timestamp (float): Time of the first sample, in seconds. duration (float): Total data duration, in seconds. - events (List[:class:`pyglet.media.events.MediaEvent`]): List of events + events (List[:class:`pyglet.media.drivers.base.MediaEvent`]): List of events contained within this packet. Events are timestamped relative to this audio packet. """ diff --git a/libs/pyglet/shapes.py b/libs/pyglet/shapes.py index bd8e876..833fdf4 100644 --- a/libs/pyglet/shapes.py +++ b/libs/pyglet/shapes.py @@ -70,12 +70,18 @@ A simple example of drawing shapes:: pyglet.app.run() +.. note:: Some Shapes, such as Lines and Triangles, have multiple coordinates. + If you update the x, y coordinate, this will also affect the secondary + coordinates. This allows you to move the shape without affecting it's + overall dimensions. .. versionadded:: 1.5.4 """ import math +from abc import ABC, abstractmethod + import pyglet from pyglet.gl import GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA @@ -85,8 +91,11 @@ from pyglet.graphics import shader, Batch, Group vertex_source = """#version 150 core - in vec2 position; + in vec2 vertices; + in vec2 translation; in vec4 colors; + in float rotation; + out vec4 vertex_colors; @@ -96,9 +105,19 @@ vertex_source = """#version 150 core mat4 view; } window; + mat4 m_rotation = mat4(1.0); + mat4 m_translate = mat4(1.0); + void main() { - gl_Position = window.projection * window.view * vec4(position, 0.0, 1.0); + m_translate[3][0] = translation.x; + m_translate[3][1] = translation.y; + m_rotation[0][0] = cos(-radians(rotation)); + m_rotation[0][1] = sin(-radians(rotation)); + m_rotation[1][0] = -sin(-radians(rotation)); + m_rotation[1][1] = cos(-radians(rotation)); + + gl_Position = window.projection * window.view * m_translate * m_rotation * vec4(vertices, 0.0, 1.0); vertex_colors = colors; } """ @@ -125,32 +144,6 @@ def get_default_shader(): return default_shader_program -def _rotate(vertices, angle, x, y): - """Rotate the vertices by the angle around x, y. - - :Parameters: - `vertices` : list - A list of (x, y) tuples, representing each vertex to rotate. - `angle` : float - The angle of the rotation in degrees. - `x` : int or float - X coordinate of the center of rotation. - `y` : int or float - Y coordinate of the center of rotation. - """ - r = -math.radians(angle) - cr = math.cos(r) - sr = math.sin(r) - - rotated_vertices = [] - for vertex in vertices: - rotated_x = (vertex[0] - x) * cr - (vertex[1] - y) * sr + x - rotated_y = (vertex[1] - y) * cr + (vertex[0] - x) * sr + y - rotated_vertices.append((rotated_x, rotated_y)) - - return rotated_vertices - - class _ShapeGroup(Group): """Shared Shape rendering Group. @@ -202,11 +195,18 @@ class _ShapeGroup(Group): return hash((id(self.parent), self.blend_src, self.blend_dest, self.order, self.program)) -class _ShapeBase: - """Base class for Shape objects""" +class ShapeBase(ABC): + """Base class for all shape objects. - _rgb = (255, 255, 255) - _opacity = 255 + A number of default shapes are provided in this module. Curves are + approximated using multiple vertices. + + If you need shapes or functionality not provided in this module, + you can write your own custom subclass of `ShapeBase` by using + the provided shapes as reference. + """ + + _rgba = (255, 255, 255, 255) _visible = True _x = 0 _y = 0 @@ -220,11 +220,45 @@ class _ShapeBase: if self._vertex_list is not None: self._vertex_list.delete() - def _update_position(self): - raise NotImplementedError - + @abstractmethod def _update_color(self): - raise NotImplementedError + """ + Send the new colors for each vertex to the GPU. + + This method must set the contents of `self._vertex_list.colors` + using a list or tuple that contains the RGBA color components + for each vertex in the shape. This is usually done by repeating + `self._rgba` for each vertex. See the `ShapeBase` subclasses in + this module for examples of how to do this. + """ + raise NotImplementedError("_update_color must be defined" + "for every ShapeBase subclass") + + @abstractmethod + def _update_position(self): + """ + Generate up-to-date vertex positions & send them to the GPU. + + This method must set the contents of `self._vertex_list.translation` + using a list or tuple that contains the new translation values for + each vertex in the shape. See the `ShapeBase` subclasses in this + module for examples of how to do this. + """ + raise NotImplementedError("_update_position must be defined" + "for every ShapeBase subclass") + + @abstractmethod + def _update_vertices(self): + """ + Generate up-to-date vertex positions & send them to the GPU. + + This method must set the contents of `self._vertex_list.vertices` + using a list or tuple that contains the new vertex coordinates for + each vertex in the shape. See the `ShapeBase` subclasses in this + module for examples of how to do this. + """ + raise NotImplementedError("_update_vertices must be defined" + "for every ShapeBase subclass") def draw(self): """Draw the shape at its current position. @@ -294,7 +328,7 @@ class _ShapeBase: @anchor_x.setter def anchor_x(self, value): self._anchor_x = value - self._update_position() + self._update_vertices() @property def anchor_y(self): @@ -307,7 +341,7 @@ class _ShapeBase: @anchor_y.setter def anchor_y(self, value): self._anchor_y = value - self._update_position() + self._update_vertices() @property def anchor_position(self): @@ -324,7 +358,7 @@ class _ShapeBase: @anchor_position.setter def anchor_position(self, values): self._anchor_x, self._anchor_y = values - self._update_position() + self._update_vertices() @property def color(self): @@ -337,11 +371,17 @@ class _ShapeBase: :type: (int, int, int) """ - return self._rgb + return self._rgba @color.setter def color(self, values): - self._rgb = tuple(map(int, values)) + r, g, b, *a = values + + if a: + self._rgba = r, g, b, a[0] + else: + self._rgba = r, g, b, self._rgba[3] + self._update_color() @property @@ -358,11 +398,11 @@ class _ShapeBase: :type: int """ - return self._opacity + return self._rgba[3] @opacity.setter def opacity(self, value): - self._opacity = value + self._rgba = (*self._rgba[:3], value) self._update_color() @property @@ -376,13 +416,12 @@ class _ShapeBase: @visible.setter def visible(self, value): self._visible = value - self._update_position() + self._update_vertices() -class Arc(_ShapeBase): +class Arc(ShapeBase): def __init__(self, x, y, radius, segments=None, angle=math.tau, start_angle=0, - closed=False, color=(255, 255, 255), opacity=255, batch=None, - group=None): + closed=False, color=(255, 255, 255, 255), batch=None, group=None): """Create an Arc. The Arc's anchor point (x, y) defaults to its center. @@ -407,12 +446,10 @@ class Arc(_ShapeBase): `closed` : bool If True, the ends of the arc will be connected with a line. defaults to False. - `color` : (int, int, int) - The RGB color of the circle, specified as a tuple of - three ints in the range of 0-255. - `opacity` : int - How opaque the arc is. The default of 255 is fully - visible. 0 is transparent. + `color` : (int, int, int, int) + The RGB or RGBA color of the arc, specified as a + tuple of 3 or 4 ints in the range of 0-255. RGB colors + will be treated as having opacity of 255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the circle to. `group` : `~pyglet.graphics.Group` @@ -424,8 +461,10 @@ class Arc(_ShapeBase): self._segments = segments or max(14, int(radius / 1.25)) self._num_verts = self._segments * 2 + (2 if closed else 0) - self._rgb = color - self._opacity = opacity + # handle both 3 and 4 byte colors + r, g, b, *a = color + self._rgba = r, g, b, a[0] if a else 255 + self._angle = angle self._start_angle = start_angle self._closed = closed @@ -435,16 +474,23 @@ class Arc(_ShapeBase): program = get_default_shader() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(self._num_verts, GL_LINES, self._batch, self._group, colors='Bn') - self._update_position() - self._update_color() + self._vertex_list = program.vertex_list(self._num_verts, GL_LINES, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (x, y) * self._num_verts)) + self._update_vertices() + + def _update_color(self): + self._vertex_list.colors[:] = self._rgba * self._num_verts def _update_position(self): + self._vertex_list.translation[:] = (self._x, self._y) * self._num_verts + + def _update_vertices(self): if not self._visible: vertices = (0,) * self._segments * 4 else: - x = self._x + self._anchor_x - y = self._y + self._anchor_y + x = -self._anchor_x + y = -self._anchor_y r = self._radius tau_segs = self._angle / self._segments start_angle = self._start_angle - math.radians(self._rotation) @@ -463,10 +509,7 @@ class Arc(_ShapeBase): chord_points = *points[-1], *points[0] vertices.extend(chord_points) - self._vertex_list.position[:] = vertices - - def _update_color(self): - self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._num_verts + self._vertex_list.vertices[:] = vertices @property def rotation(self): @@ -482,7 +525,7 @@ class Arc(_ShapeBase): @rotation.setter def rotation(self, rotation): self._rotation = rotation - self._update_position() + self._vertex_list.rotation[:] = (rotation,) * self._num_verts def draw(self): """Draw the shape at its current position. @@ -493,8 +536,8 @@ class Arc(_ShapeBase): self._vertex_list.draw(GL_LINES) -class Circle(_ShapeBase): - def __init__(self, x, y, radius, segments=None, color=(255, 255, 255), opacity=255, +class Circle(ShapeBase): + def __init__(self, x, y, radius, segments=None, color=(255, 255, 255, 255), batch=None, group=None): """Create a circle. @@ -512,12 +555,10 @@ class Circle(_ShapeBase): the circle should be made from. If not specified it will be automatically calculated using the formula: `max(14, int(radius / 1.25))`. - `color` : (int, int, int) - The RGB color of the circle, specified as a tuple of - three ints in the range of 0-255. - `opacity` : int - How opaque the circle is. The default of 255 is fully - visible. 0 is transparent. + `color` : (int, int, int, int) + The RGB or RGBA color of the circle, specified as a + tuple of 3 or 4 ints in the range of 0-255. RGB colors + will be treated as having an opacity of 255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the circle to. `group` : `~pyglet.graphics.Group` @@ -527,23 +568,31 @@ class Circle(_ShapeBase): self._y = y self._radius = radius self._segments = segments or max(14, int(radius / 1.25)) - self._rgb = color - self._opacity = opacity + self._num_verts = self._segments * 3 + r, g, b, *a = color + self._rgba = r, g, b, a[0] if a else 255 program = get_default_shader() self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(self._segments*3, GL_TRIANGLES, self._batch, self._group, colors='Bn') - self._update_position() - self._update_color() + self._vertex_list = program.vertex_list(self._segments*3, GL_TRIANGLES, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (x, y) * self._num_verts)) + self._update_vertices() + + def _update_color(self): + self._vertex_list.colors[:] = self._rgba * self._num_verts def _update_position(self): + self._vertex_list.translation[:] = (self._x, self._y) * self._num_verts + + def _update_vertices(self): if not self._visible: vertices = (0,) * self._segments * 6 else: - x = self._x + self._anchor_x - y = self._y + self._anchor_y + x = -self._anchor_x + y = -self._anchor_y r = self._radius tau_segs = math.pi * 2 / self._segments @@ -557,10 +606,7 @@ class Circle(_ShapeBase): triangle = x, y, *points[i - 1], *point vertices.extend(triangle) - self._vertex_list.position[:] = vertices - - def _update_color(self): - self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._segments * 3 + self._vertex_list.vertices[:] = vertices @property def radius(self): @@ -573,11 +619,11 @@ class Circle(_ShapeBase): @radius.setter def radius(self, value): self._radius = value - self._update_position() + self._update_vertices() -class Ellipse(_ShapeBase): - def __init__(self, x, y, a, b, color=(255, 255, 255), opacity=255, +class Ellipse(ShapeBase): + def __init__(self, x, y, a, b, color=(255, 255, 255, 255), batch=None, group=None): """Create an ellipse. @@ -592,12 +638,10 @@ class Ellipse(_ShapeBase): Semi-major axes of the ellipse. `b`: float Semi-minor axes of the ellipse. - `color` : (int, int, int) - The RGB color of the ellipse. specify as a tuple of - three ints in the range of 0~255. - `opacity` : int - How opaque the ellipse is. The default of 255 is fully - visible. 0 is transparent. + `color` : (int, int, int, int) + The RGB or RGBA color of the ellipse, specified as a + tuple of 3 or 4 ints in the range of 0-255. RGB colors + will be treated as having an opacity of 255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the circle to. `group` : `~pyglet.graphics.Group` @@ -607,8 +651,12 @@ class Ellipse(_ShapeBase): self._y = y self._a = a self._b = b - self._rgb = color - self._opacity = opacity + + # Break with conventions in other _Shape constructors + # because a & b are used as meaningful variable names. + color_r, color_g, color_b, *color_a = color + self._rgba = color_r, color_g, color_b, color_a[0] if color_a else 255 + self._rotation = 0 self._segments = int(max(a, b) / 1.25) self._num_verts = self._segments * 2 @@ -617,35 +665,36 @@ class Ellipse(_ShapeBase): self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(self._num_verts, GL_LINES, self._batch, self._group, colors='Bn') - self._update_position() - self._update_color() + self._vertex_list = program.vertex_list(self._num_verts, GL_LINES, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (x, y) * self._num_verts)) + self._update_vertices() + + def _update_color(self): + self._vertex_list.colors[:] = self._rgba * self._num_verts def _update_position(self): + self._vertex_list.translation[:] = (self._x, self._y) * self._num_verts + + def _update_vertices(self): if not self._visible: vertices = (0,) * self._num_verts * 4 else: - x = self._x + self._anchor_x - y = self._y + self._anchor_y + x = -self._anchor_x + y = -self._anchor_y tau_segs = math.pi * 2 / self._segments # Calculate the points of the ellipse by formula: points = [(x + self._a * math.cos(i * tau_segs), y + self._b * math.sin(i * tau_segs)) for i in range(self._segments + 1)] - # Rotate all points: - if self._rotation: - points = _rotate(points, self._rotation, x, y) - # Create a list of lines from the points: vertices = [] for i in range(len(points) - 1): line_points = *points[i], *points[i + 1] vertices.extend(line_points) - self._vertex_list.position[:] = vertices - def _update_color(self): - self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._num_verts + self._vertex_list.vertices[:] = vertices @property def a(self): @@ -658,7 +707,7 @@ class Ellipse(_ShapeBase): @a.setter def a(self, value): self._a = value - self._update_position() + self._update_vertices() @property def b(self): @@ -671,7 +720,7 @@ class Ellipse(_ShapeBase): @b.setter def b(self, value): self._b = value - self._update_position() + self._update_vertices() @property def rotation(self): @@ -687,7 +736,7 @@ class Ellipse(_ShapeBase): @rotation.setter def rotation(self, rotation): self._rotation = rotation - self._update_position() + self._vertex_list.rotation[:] = (rotation,) * self._num_verts def draw(self): """Draw the shape at its current position. @@ -698,9 +747,9 @@ class Ellipse(_ShapeBase): self._vertex_list.draw(GL_LINES) -class Sector(_ShapeBase): +class Sector(ShapeBase): def __init__(self, x, y, radius, segments=None, angle=math.tau, start_angle=0, - color=(255, 255, 255), opacity=255, batch=None, group=None): + color=(255, 255, 255, 255), batch=None, group=None): """Create a Sector of a circle. The sector's anchor point (x, y) defaults to the center of the circle. @@ -722,12 +771,10 @@ class Sector(_ShapeBase): which is a full circle. `start_angle` : float The start angle of the sector, in radians. Defaults to 0. - `color` : (int, int, int) - The RGB color of the sector, specified as a tuple of - three ints in the range of 0-255. - `opacity` : int - How opaque the sector is. The default of 255 is fully - visible. 0 is transparent. + `color` : (int, int, int, int) + The RGB or RGBA color of the circle, specified as a + tuple of 3 or 4 ints in the range of 0-255. RGB colors + will be treated as having an opacity of 255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the sector to. `group` : `~pyglet.graphics.Group` @@ -737,9 +784,11 @@ class Sector(_ShapeBase): self._y = y self._radius = radius self._segments = segments or max(14, int(radius / 1.25)) + self._num_verts = self._segments * 3 + + r, g, b, *a = color + self._rgba = r, g, b, a[0] if a else 255 - self._rgb = color - self._opacity = opacity self._angle = angle self._start_angle = start_angle self._rotation = 0 @@ -748,16 +797,23 @@ class Sector(_ShapeBase): self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(self._segments*3, GL_TRIANGLES, self._batch, self._group, colors='Bn') - self._update_position() - self._update_color() + self._vertex_list = program.vertex_list(self._num_verts, GL_TRIANGLES, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (x, y) * self._num_verts)) + self._update_vertices() + + def _update_color(self): + self._vertex_list.colors[:] = self._rgba * self._num_verts def _update_position(self): + self._vertex_list.translation[:] = (self._x, self._y) * self._num_verts + + def _update_vertices(self): if not self._visible: vertices = (0,) * self._segments * 6 else: - x = self._x + self._anchor_x - y = self._y + self._anchor_y + x = -self._anchor_x + y = -self._anchor_y r = self._radius tau_segs = self._angle / self._segments start_angle = self._start_angle - math.radians(self._rotation) @@ -772,10 +828,7 @@ class Sector(_ShapeBase): triangle = x, y, *points[i - 1], *point vertices.extend(triangle) - self._vertex_list.position[:] = vertices - - def _update_color(self): - self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._segments * 3 + self._vertex_list.vertices[:] = vertices @property def angle(self): @@ -788,7 +841,7 @@ class Sector(_ShapeBase): @angle.setter def angle(self, value): self._angle = value - self._update_position() + self._update_vertices() @property def start_angle(self): @@ -801,7 +854,7 @@ class Sector(_ShapeBase): @start_angle.setter def start_angle(self, angle): self._start_angle = angle - self._update_position() + self._update_vertices() @property def radius(self): @@ -814,7 +867,7 @@ class Sector(_ShapeBase): @radius.setter def radius(self, value): self._radius = value - self._update_position() + self._update_vertices() @property def rotation(self): @@ -830,11 +883,11 @@ class Sector(_ShapeBase): @rotation.setter def rotation(self, rotation): self._rotation = rotation - self._update_position() + self._vertex_list.rotation[:] = (rotation,) * self._num_verts -class Line(_ShapeBase): - def __init__(self, x, y, x2, y2, width=1, color=(255, 255, 255), opacity=255, +class Line(ShapeBase): + def __init__(self, x, y, x2, y2, width=1, color=(255, 255, 255, 255), batch=None, group=None): """Create a line. @@ -852,12 +905,10 @@ class Line(_ShapeBase): The second Y coordinate of the line. `width` : float The desired width of the line. - `color` : (int, int, int) - The RGB color of the line, specified as a tuple of - three ints in the range of 0-255. - `opacity` : int - How opaque the line is. The default of 255 is fully - visible. 0 is transparent. + `color` : (int, int, int, int) + The RGB or RGBA color of the line, specified as a + tuple of 3 or 4 ints in the range of 0-255. RGB colors + will be treated as having an opacity of 255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the line to. `group` : `~pyglet.graphics.Group` @@ -870,43 +921,48 @@ class Line(_ShapeBase): self._width = width self._rotation = math.degrees(math.atan2(y2 - y, x2 - x)) - self._rgb = color - self._opacity = opacity + self._num_verts = 6 + + r, g, b, *a = color + self._rgba = r, g, b, a[0] if a else 255 program = get_default_shader() self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(6, GL_TRIANGLES, self._batch, self._group, colors='Bn') - self._update_position() - self._update_color() - - def _update_position(self): - if not self._visible: - self._vertex_list.position[:] = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - else: - x1 = -self._anchor_y - y1 = self._anchor_x - self._width / 2 - x = self._x - y = self._y - x2 = x1 + math.hypot(self._y2 - y, self._x2 - x) - y2 = y1 + self._width - - r = math.atan2(self._y2 - y, self._x2 - x) - cr = math.cos(r) - sr = math.sin(r) - ax = x1 * cr - y1 * sr + x - ay = x1 * sr + y1 * cr + y - bx = x2 * cr - y1 * sr + x - by = x2 * sr + y1 * cr + y - cx = x2 * cr - y2 * sr + x - cy = x2 * sr + y2 * cr + y - dx = x1 * cr - y2 * sr + x - dy = x1 * sr + y2 * cr + y - self._vertex_list.position[:] = (ax, ay, bx, by, cx, cy, ax, ay, cx, cy, dx, dy) + self._vertex_list = program.vertex_list(6, GL_TRIANGLES, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (x, y) * self._num_verts)) + self._update_vertices() def _update_color(self): - self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * 6 + self._vertex_list.colors[:] = self._rgba * self._num_verts + + def _update_position(self): + self._vertex_list.translation[:] = (self._x, self._y) * self._num_verts + + def _update_vertices(self): + if not self._visible: + self._vertex_list.vertices[:] = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + else: + x1 = -self._anchor_x + y1 = self._anchor_y - self._width / 2 + x2 = x1 + math.hypot(self._y2 - self._y, self._x2 - self._x) + y2 = y1 + self._width + + r = math.atan2(self._y2 - self._y, self._x2 - self._x) + cr = math.cos(r) + sr = math.sin(r) + ax = x1 * cr - y1 * sr + ay = x1 * sr + y1 * cr + bx = x2 * cr - y1 * sr + by = x2 * sr + y1 * cr + cx = x2 * cr - y2 * sr + cy = x2 * sr + y2 * cr + dx = x1 * cr - y2 * sr + dy = x1 * sr + y2 * cr + + self._vertex_list.vertices[:] = (ax, ay, bx, by, cx, cy, ax, ay, cx, cy, dx, dy) @property def x2(self): @@ -919,7 +975,7 @@ class Line(_ShapeBase): @x2.setter def x2(self, value): self._x2 = value - self._update_position() + self._update_vertices() @property def y2(self): @@ -932,32 +988,11 @@ class Line(_ShapeBase): @y2.setter def y2(self, value): self._y2 = value - self._update_position() - - @property - def position(self): - """The (x, y, x2, y2) coordinates of the line, as a tuple. - - :Parameters: - `x` : int or float - X coordinate of the line. - `y` : int or float - Y coordinate of the line. - `x2` : int or float - X2 coordinate of the line. - `y2` : int or float - Y2 coordinate of the line. - """ - return self._x, self._y, self._x2, self._y2 - - @position.setter - def position(self, values): - self._x, self._y, self._x2, self._y2 = values - self._update_position() + self._update_vertices() -class Rectangle(_ShapeBase): - def __init__(self, x, y, width, height, color=(255, 255, 255), opacity=255, +class Rectangle(ShapeBase): + def __init__(self, x, y, width, height, color=(255, 255, 255, 255), batch=None, group=None): """Create a rectangle or square. @@ -973,12 +1008,10 @@ class Rectangle(_ShapeBase): The width of the rectangle. `height` : float The height of the rectangle. - `color` : (int, int, int) - The RGB color of the rectangle, specified as - a tuple of three ints in the range of 0-255. - `opacity` : int - How opaque the rectangle is. The default of 255 is fully - visible. 0 is transparent. + `color` : (int, int, int, int) + The RGB or RGBA color of the circle, specified as a + tuple of 3 or 4 ints in the range of 0-255. RGB colors + will be treated as having an opacity of 255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the rectangle to. `group` : `~pyglet.graphics.Group` @@ -989,37 +1022,36 @@ class Rectangle(_ShapeBase): self._width = width self._height = height self._rotation = 0 - self._rgb = color - self._opacity = opacity + self._num_verts = 6 + + r, g, b, *a = color + self._rgba = r, g, b, a[0] if a else 255 program = get_default_shader() self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(6, GL_TRIANGLES, self._batch, self._group, colors='Bn') - self._update_position() - self._update_color() - - def _update_position(self): - if not self._visible: - self._vertex_list.position[:] = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - else: - x1 = self._x - self._anchor_x - y1 = self._y - self._anchor_y - x2 = x1 + self._width - y2 = y1 + self._height - x = self._x - y = self._y - - vertices = [(x1, y1), (x2, y1), (x2, y2), (x1, y1), (x2, y2), (x1, y2)] - - if self._rotation: - vertices = _rotate(vertices, self._rotation, x, y) - - self._vertex_list.position[:] = tuple(value for vertex in vertices for value in vertex) + self._vertex_list = program.vertex_list(6, GL_TRIANGLES, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (x, y) * self._num_verts)) + self._update_vertices() def _update_color(self): - self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * 6 + self._vertex_list.colors[:] = self._rgba * self._num_verts + + def _update_position(self): + self._vertex_list.translation[:] = (self._x, self._y) * self._num_verts + + def _update_vertices(self): + if not self._visible: + self._vertex_list.vertices[:] = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + else: + x1 = -self._anchor_x + y1 = -self._anchor_y + x2 = x1 + self._width + y2 = y1 + self._height + + self._vertex_list.vertices[:] = x1, y1, x2, y1, x2, y2, x1, y1, x2, y2, x1, y2 @property def width(self): @@ -1032,7 +1064,7 @@ class Rectangle(_ShapeBase): @width.setter def width(self, value): self._width = value - self._update_position() + self._update_vertices() @property def height(self): @@ -1045,7 +1077,7 @@ class Rectangle(_ShapeBase): @height.setter def height(self, value): self._height = value - self._update_position() + self._update_vertices() @property def rotation(self): @@ -1061,12 +1093,12 @@ class Rectangle(_ShapeBase): @rotation.setter def rotation(self, rotation): self._rotation = rotation - self._update_position() + self._vertex_list.rotation[:] = (rotation,) * self._num_verts -class BorderedRectangle(_ShapeBase): +class BorderedRectangle(ShapeBase): def __init__(self, x, y, width, height, border=1, color=(255, 255, 255), - border_color=(100, 100, 100), opacity=255, batch=None, group=None): + border_color=(100, 100, 100), batch=None, group=None): """Create a rectangle or square. The rectangle's anchor point defaults to the (x, y) coordinates, @@ -1083,16 +1115,19 @@ class BorderedRectangle(_ShapeBase): The height of the rectangle. `border` : float The thickness of the border. - `color` : (int, int, int) - The RGB color of the rectangle, specified as - a tuple of three ints in the range of 0-255. - `border_color` : (int, int, int) - The RGB color of the rectangle's border, specified as - a tuple of three ints in the range of 0-255. - `opacity` : int - How opaque the rectangle is. The default of 255 is fully - visible. 0 is transparent. This affects the entire shape, - not only the fill color. + `color` : (int, int, int, int) + The RGB or RGBA fill color of the rectangle, specified + as a tuple of 3 or 4 ints in the range of 0-255. RGB + colors will be treated as having an opacity of 255. + `border_color` : (int, int, int, int) + The RGB or RGBA fill color of the rectangle, specified + as a tuple of 3 or 4 ints in the range of 0-255. RGB + colors will be treated as having an opacity of 255. + + The alpha values must match if you pass RGBA values to + both this argument and `border_color`. If they do not, + a `ValueError` will be raised informing you of the + ambiguity. `batch` : `~pyglet.graphics.Batch` Optional batch to add the rectangle to. `group` : `~pyglet.graphics.Group` @@ -1104,49 +1139,62 @@ class BorderedRectangle(_ShapeBase): self._height = height self._rotation = 0 self._border = border - self._rgb = color - self._brgb = border_color - self._opacity = opacity + self._num_verts = 8 + + fill_r, fill_g, fill_b, *fill_a = color + border_r, border_g, border_b, *border_a = border_color + + # Start with a default alpha value of 255. + alpha = 255 + # Raise Exception if we have conflicting alpha values + if fill_a and border_a and fill_a[0] != border_a[0]: + raise ValueError("When color and border_color are both RGBA values," + "they must both have the same opacity") + + # Choose a value to use if there is no conflict + elif fill_a: + alpha = fill_a[0] + elif border_a: + alpha = border_a[0] + + # Although the shape is only allowed one opacity, the alpha is + # stored twice to keep other code concise and reduce cpu usage + # from stitching together sequences. + self._rgba = fill_r, fill_g, fill_b, alpha + self._border_rgba = border_r, border_g, border_b, alpha program = get_default_shader() self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) indices = [0, 1, 2, 0, 2, 3, 0, 4, 3, 4, 7, 3, 0, 1, 5, 0, 5, 4, 1, 2, 5, 5, 2, 6, 6, 2, 3, 6, 3, 7] - self._vertex_list = program.vertex_list_indexed( - 8, GL_TRIANGLES, indices, self._batch, self._group, colors='Bn') - self._update_position() - self._update_color() + self._vertex_list = program.vertex_list_indexed(8, GL_TRIANGLES, indices, self._batch, self._group, + colors=('Bn', self._rgba * 4 + self._border_rgba * 4), + translation=('f', (x, y) * self._num_verts)) + self._update_vertices() + + def _update_color(self): + self._vertex_list.colors[:] = self._rgba * 4 + self._border_rgba * 4 def _update_position(self): - if not self._visible: - self._vertex_list.position[:] = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - else: - b = self._border - x = self._x - y = self._y + self._vertex_list.translation[:] = (self._x, self._y) * self._num_verts - bx1 = x - self._anchor_x - by1 = y - self._anchor_y + def _update_vertices(self): + if not self._visible: + self._vertex_list.vertices[:] = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + else: + bx1 = -self._anchor_x + by1 = -self._anchor_y bx2 = bx1 + self._width by2 = by1 + self._height + b = self._border ix1 = bx1 + b iy1 = by1 + b ix2 = bx2 - b iy2 = by2 - b - vertices = [(ix1, iy1), (ix2, iy1), (ix2, iy2), (ix1, iy2), - (bx1, by1), (bx2, by1), (bx2, by2), (bx1, by2)] - - if self._rotation: - vertices = _rotate(vertices, self._rotation, x, y) - - # Flattening the list. - self._vertex_list.position[:] = tuple(value for vertex in vertices for value in vertex) - - def _update_color(self): - opacity = int(self._opacity) - self._vertex_list.colors[:] = [*self._rgb, opacity] * 4 + [*self._brgb, opacity] * 4 + self._vertex_list.vertices[:] = (ix1, iy1, ix2, iy1, ix2, iy2, ix1, iy2, + bx1, by1, bx2, by1, bx2, by2, bx1, by2) @property def width(self): @@ -1159,7 +1207,7 @@ class BorderedRectangle(_ShapeBase): @width.setter def width(self, value): self._width = value - self._update_position() + self._update_vertices() @property def height(self): @@ -1172,7 +1220,7 @@ class BorderedRectangle(_ShapeBase): @height.setter def height(self, value): self._height = value - self._update_position() + self._update_vertices() @property def rotation(self): @@ -1186,9 +1234,9 @@ class BorderedRectangle(_ShapeBase): return self._rotation @rotation.setter - def rotation(self, value): - self._rotation = value - self._update_position() + def rotation(self, rotation): + self._rotation = rotation + self._vertex_list.rotation[:] = (rotation,) * self._num_verts @property def border_color(self): @@ -1196,21 +1244,64 @@ class BorderedRectangle(_ShapeBase): This property sets the color of the border of a bordered rectangle. - The color is specified as an RGB tuple of integers '(red, green, blue)'. + The color is specified as an RGB tuple of integers '(red, green, blue)' + or an RGBA tuple of integers '(red, green, blue, alpha)`. Setting the + alpha on this property will change the alpha of the entire shape, + including both the fill and the border. + Each color component must be in the range 0 (dark) to 255 (saturated). - :type: (int, int, int) + :type: (int, int, int, int) """ - return self._brgb + return self._border_rgba @border_color.setter def border_color(self, values): - self._brgb = tuple(map(int, values)) + r, g, b, *a = values + + if a: + alpha = a[0] + else: + alpha = self._rgba[3] + + self._border_rgba = r, g, b, alpha + self._rgba = *self._rgba[:3], alpha + + self._update_color() + + @property + def color(self): + """The rectangle's fill color. + + This property sets the color of the inside of a bordered rectangle. + + The color is specified as an RGB tuple of integers '(red, green, blue)' + or an RGBA tuple of integers '(red, green, blue, alpha)`. Setting the + alpha on this property will change the alpha of the entire shape, + including both the fill and the border. + + Each color component must be in the range 0 (dark) to 255 (saturated). + + :type: (int, int, int, int) + """ + return self._rgba + + @color.setter + def color(self, values): + r, g, b, *a = values + + if a: + alpha = a[0] + else: + alpha = self._rgba[3] + + self._rgba = r, g, b, alpha + self._border_rgba = *self._border_rgba[:3], alpha self._update_color() -class Triangle(_ShapeBase): - def __init__(self, x, y, x2, y2, x3, y3, color=(255, 255, 255), opacity=255, +class Triangle(ShapeBase): + def __init__(self, x, y, x2, y2, x3, y3, color=(255, 255, 255, 255), batch=None, group=None): """Create a triangle. @@ -1229,12 +1320,10 @@ class Triangle(_ShapeBase): The third X coordinate of the triangle. `y3` : float The third Y coordinate of the triangle. - `color` : (int, int, int) - The RGB color of the triangle, specified as - a tuple of three ints in the range of 0-255. - `opacity` : int - How opaque the triangle is. The default of 255 is fully - visible. 0 is transparent. + `color` : (int, int, int, int) + The RGB or RGBA color of the triangle, specified as a + tuple of 3 or 4 ints in the range of 0-255. RGB colors + will be treated as having an opacity of 255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the triangle to. `group` : `~pyglet.graphics.Group` @@ -1247,33 +1336,37 @@ class Triangle(_ShapeBase): self._x3 = x3 self._y3 = y3 self._rotation = 0 - self._rgb = color - self._opacity = opacity + self._num_verts = 3 + + r, g, b, *a = color + self._rgba = r, g, b, a[0] if a else 255 program = get_default_shader() self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(3, GL_TRIANGLES, self._batch, self._group, colors='Bn') - self._update_position() - self._update_color() - - def _update_position(self): - if not self._visible: - self._vertex_list.position[:] = (0, 0, 0, 0, 0, 0) - else: - anchor_x = self._anchor_x - anchor_y = self._anchor_y - x1 = self._x - anchor_x - y1 = self._y - anchor_y - x2 = self._x2 - anchor_x - y2 = self._y2 - anchor_y - x3 = self._x3 - anchor_x - y3 = self._y3 - anchor_y - self._vertex_list.position[:] = (x1, y1, x2, y2, x3, y3) + self._vertex_list = program.vertex_list(3, GL_TRIANGLES, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (x, y) * self._num_verts)) + self._update_vertices() def _update_color(self): - self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * 3 + self._vertex_list.colors[:] = self._rgba * self._num_verts + + def _update_position(self): + self._vertex_list.translation[:] = (self._x, self._y) * 3 + + def _update_vertices(self): + if not self._visible: + self._vertex_list.vertices[:] = (0, 0, 0, 0, 0, 0) + else: + x1 = -self._anchor_x + y1 = -self._anchor_y + x2 = self._x2 + x1 - self._x + y2 = self._y2 + y1 - self._y + x3 = self._x3 + x1 - self._x + y3 = self._y3 + y1 - self._y + self._vertex_list.vertices[:] = (x1, y1, x2, y2, x3, y3) @property def x2(self): @@ -1281,12 +1374,12 @@ class Triangle(_ShapeBase): :type: int or float """ - return self._x2 + return self._x + self._x2 @x2.setter def x2(self, value): self._x2 = value - self._update_position() + self._update_vertices() @property def y2(self): @@ -1294,12 +1387,12 @@ class Triangle(_ShapeBase): :type: int or float """ - return self._y2 + return self._y + self._y2 @y2.setter def y2(self, value): self._y2 = value - self._update_position() + self._update_vertices() @property def x3(self): @@ -1307,12 +1400,12 @@ class Triangle(_ShapeBase): :type: int or float """ - return self._x3 + return self._x + self._x3 @x3.setter def x3(self, value): self._x3 = value - self._update_position() + self._update_vertices() @property def y3(self): @@ -1320,42 +1413,17 @@ class Triangle(_ShapeBase): :type: int or float """ - return self._y3 + return self._y + self._y3 @y3.setter def y3(self, value): self._y3 = value - self._update_position() - - @property - def position(self): - """The (x, y, x2, y2, x3, y3) coordinates of the triangle, as a tuple. - - :Parameters: - `x` : int or float - X coordinate of the triangle. - `y` : int or float - Y coordinate of the triangle. - `x2` : int or float - X2 coordinate of the triangle. - `y2` : int or float - Y2 coordinate of the triangle. - `x3` : int or float - X3 coordinate of the triangle. - `y3` : int or float - Y3 coordinate of the triangle. - """ - return self._x, self._y, self._x2, self._y2, self._x3, self._y3 - - @position.setter - def position(self, values): - self._x, self._y, self._x2, self._y2, self._x3, self._y3 = values - self._update_position() + self._update_vertices() -class Star(_ShapeBase): +class Star(ShapeBase): def __init__(self, x, y, outer_radius, inner_radius, num_spikes, rotation=0, - color=(255, 255, 255), opacity=255, batch=None, group=None) -> None: + color=(255, 255, 255, 255), batch=None, group=None) -> None: """Create a star. The star's anchor point (x, y) defaults to the center of the star. @@ -1376,11 +1444,9 @@ class Star(_ShapeBase): will result in one spike lining up with the X axis in positive direction. `color` : (int, int, int) - The RGB color of the star, specified as - a tuple of three ints in the range of 0-255. - `opacity` : int - How opaque the star is. The default of 255 is fully - visible. 0 is transparent. + The RGB or RGBA color of the star, specified as a + tuple of 3 or 4 ints in the range of 0-255. RGB colors + will be treated as having an opacity of 255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the star to. `group` : `~pyglet.graphics.Group` @@ -1391,40 +1457,47 @@ class Star(_ShapeBase): self._outer_radius = outer_radius self._inner_radius = inner_radius self._num_spikes = num_spikes - self._rgb = color - self._opacity = opacity + self._num_verts = num_spikes * 6 self._rotation = rotation + r, g, b, *a = color + self._rgba = r, g, b, a[0] if a else 255 + program = get_default_shader() self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - self._vertex_list = program.vertex_list(self._num_spikes*6, GL_TRIANGLES, self._batch, self._group, colors='Bn') - self._update_position() - self._update_color() + self._vertex_list = program.vertex_list(self._num_verts, GL_TRIANGLES, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + rotation=('f', (rotation,) * self._num_verts), + translation=('f', (x, y) * self._num_verts)) + self._update_vertices() + + def _update_color(self): + self._vertex_list.colors[:] = self._rgba * self._num_verts def _update_position(self): + self._vertex_list.translation[:] = (self._x, self._y) * self._num_verts + + def _update_vertices(self): if not self._visible: vertices = (0, 0) * self._num_spikes * 6 else: - x = self._x + self._anchor_x - y = self._y + self._anchor_y + x = -self._anchor_x + y = -self._anchor_y r_i = self._inner_radius r_o = self._outer_radius # get angle covered by each line (= half a spike) d_theta = math.pi / self._num_spikes - # phase shift rotation - phi = self._rotation / 180 * math.pi - # calculate alternating points on outer and outer circles points = [] for i in range(self._num_spikes): - points.append((x + (r_o * math.cos(2*i * d_theta + phi)), - y + (r_o * math.sin(2*i * d_theta + phi)))) - points.append((x + (r_i * math.cos((2*i+1) * d_theta + phi)), - y + (r_i * math.sin((2*i+1) * d_theta + phi)))) + points.append((x + (r_o * math.cos(2*i * d_theta)), + y + (r_o * math.sin(2*i * d_theta)))) + points.append((x + (r_i * math.cos((2*i+1) * d_theta)), + y + (r_i * math.sin((2*i+1) * d_theta)))) # create a list of doubled-up points from the points vertices = [] @@ -1432,10 +1505,7 @@ class Star(_ShapeBase): triangle = x, y, *points[i - 1], *point vertices.extend(triangle) - self._vertex_list.position[:] = vertices - - def _update_color(self): - self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * self._num_spikes * 6 + self._vertex_list.vertices[:] = vertices @property def outer_radius(self): @@ -1445,7 +1515,7 @@ class Star(_ShapeBase): @outer_radius.setter def outer_radius(self, value): self._outer_radius = value - self._update_position() + self._update_vertices() @property def inner_radius(self): @@ -1455,7 +1525,7 @@ class Star(_ShapeBase): @inner_radius.setter def inner_radius(self, value): self._inner_radius = value - self._update_position() + self._update_vertices() @property def num_spikes(self): @@ -1465,7 +1535,7 @@ class Star(_ShapeBase): @num_spikes.setter def num_spikes(self, value): self._num_spikes = value - self._update_position() + self._update_vertices() @property def rotation(self): @@ -1476,11 +1546,11 @@ class Star(_ShapeBase): @rotation.setter def rotation(self, rotation): self._rotation = rotation - self._update_position() + self._vertex_list.rotation[:] = (rotation,) * self._num_verts -class Polygon(_ShapeBase): - def __init__(self, *coordinates, color=(255, 255, 255), opacity=255, batch=None, group=None): +class Polygon(ShapeBase): + def __init__(self, *coordinates, color=(255, 255, 255, 255), batch=None, group=None): """Create a convex polygon. The polygon's anchor point defaults to the first vertex point. @@ -1489,11 +1559,9 @@ class Polygon(_ShapeBase): `coordinates` : List[[int, int]] The coordinates for each point in the polygon. `color` : (int, int, int) - The RGB color of the polygon, specified as - a tuple of three ints in the range of 0-255. - `opacity` : int - How opaque the polygon is. The default of 255 is fully - visible. 0 is transparent. + The RGB or RGBA color of the polygon, specified as a + tuple of 3 or 4 ints in the range of 0-255. RGB colors + will be treated as having an opacity of 255. `batch` : `~pyglet.graphics.Batch` Optional batch to add the polygon to. `group` : `~pyglet.graphics.Group` @@ -1501,36 +1569,38 @@ class Polygon(_ShapeBase): """ # len(self._coordinates) = the number of vertices and sides in the shape. - self._coordinates = list(coordinates) - self._rotation = 0 + self._coordinates = list(coordinates) + self._num_verts = (len(self._coordinates) - 2) * 3 - self._rgb = color - self._opacity = opacity + r, g, b, *a = color + self._rgba = r, g, b, a[0] if a else 255 program = get_default_shader() self._batch = batch or Batch() self._group = _ShapeGroup(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) - length = (len(self._coordinates) - 2) * 3 - self._vertex_list = program.vertex_list(length, GL_TRIANGLES, self._batch, self._group, colors='Bn') - - self._update_position() + self._vertex_list = program.vertex_list(self._num_verts, GL_TRIANGLES, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (coordinates[0]) * self._num_verts)) + self._update_vertices() self._update_color() + def _update_color(self): + self._vertex_list.colors[:] = self._rgba * self._num_verts + def _update_position(self): + self._vertex_list.translation[:] = (self._x, self._y) * self._num_verts + + def _update_vertices(self): if not self._visible: - self._vertex_list.position[:] = tuple([0] * ((len(self._coordinates) - 2) * 6)) + self._vertex_list.vertices[:] = tuple([0] * ((len(self._coordinates) - 2) * 6)) else: # Adjust all coordinates by the anchor. - anchor_x = self._anchor_x - anchor_y = self._anchor_y - coords = [[x - anchor_x, y - anchor_y] for x, y in self._coordinates] - - if self._rotation: - # Rotate the polygon around its first vertex. - x, y = self._coordinates[0] - coords = _rotate(coords, self._rotation, x, y) + trans_x, trans_y = self._coordinates[0] + trans_x += self._anchor_x + trans_y += self._anchor_y + coords = [[x - trans_x, y - trans_y] for x, y in self._coordinates] # Triangulate the convex polygon. triangles = [] @@ -1538,53 +1608,7 @@ class Polygon(_ShapeBase): triangles += [coords[0], coords[n + 1], coords[n + 2]] # Flattening the list before setting vertices to it. - self._vertex_list.position[:] = tuple(value for coordinate in triangles for value in coordinate) - - def _update_color(self): - self._vertex_list.colors[:] = [*self._rgb, int(self._opacity)] * ((len(self._coordinates) - 2) * 3) - - @property - def x(self): - """X coordinate of the shape. - - :type: int or float - """ - return self._coordinates[0][0] - - @x.setter - def x(self, value): - self._coordinates[0][0] = value - self._update_position() - - @property - def y(self): - """Y coordinate of the shape. - - :type: int or float - """ - return self._coordinates[0][1] - - @y.setter - def y(self, value): - self._coordinates[0][1] = value - self._update_position() - - @property - def position(self): - """The (x, y) coordinates of the shape, as a tuple. - - :Parameters: - `x` : int or float - X coordinate of the shape. - `y` : int or float - Y coordinate of the shape. - """ - return self._coordinates[0][0], self._coordinates[0][1] - - @position.setter - def position(self, values): - self._coordinates[0][0], self._coordinates[0][1] = values - self._update_position() + self._vertex_list.vertices[:] = tuple(value for coordinate in triangles for value in coordinate) @property def rotation(self): @@ -1600,7 +1624,7 @@ class Polygon(_ShapeBase): @rotation.setter def rotation(self, rotation): self._rotation = rotation - self._update_position() + self._vertex_list.rotation[:] = (rotation,) * self._num_verts -__all__ = ('Arc', 'Circle', 'Ellipse', 'Line', 'Rectangle', 'BorderedRectangle', 'Triangle', 'Star', 'Polygon', 'Sector') +__all__ = 'Arc', 'Circle', 'Ellipse', 'Line', 'Rectangle', 'BorderedRectangle', 'Triangle', 'Star', 'Polygon', 'Sector' diff --git a/libs/pyglet/text/caret.py b/libs/pyglet/text/caret.py index 885cff0..a142a66 100644 --- a/libs/pyglet/text/caret.py +++ b/libs/pyglet/text/caret.py @@ -356,6 +356,7 @@ class Caret: m2 = len(self._layout.document.text) else: m2 = m2.start() + self._position = m2 self._update(line=line) self._next_attributes.clear() @@ -436,6 +437,7 @@ class Caret: elif self._position > 0: self._position -= 1 self._layout.document.delete_text(self._position, self._position + 1) + self._update() elif motion == key.MOTION_DELETE: if self.mark is not None: self._delete_selection() diff --git a/libs/pyglet/text/layout.py b/libs/pyglet/text/layout.py index 0faed1f..b2c4e43 100644 --- a/libs/pyglet/text/layout.py +++ b/libs/pyglet/text/layout.py @@ -347,7 +347,7 @@ class _GlyphBox(_AbstractBox): v2 += x1 v1 += y + baseline v3 += y + baseline - vertices.extend(map(int, [v0, v1, v2, v1, v2, v3, v0, v3])) + vertices.extend(map(round, [v0, v1, v2, v1, v2, v3, v0, v3])) t = glyph.tex_coords tex_coords.extend(t) x1 += glyph.advance @@ -412,17 +412,19 @@ class _GlyphBox(_AbstractBox): if background_vertices: background_indices = [] bg_count = len(background_vertices) // 2 - for glyph_idx in range(bg_count): - background_indices.extend([element + (glyph_idx * 4) for element in [0, 1, 2, 0, 2, 3]]) + decoration_program = get_default_decoration_shader() + for bg_idx in range(bg_count): + background_indices.extend([element + (bg_idx * 4) for element in [0, 1, 2, 0, 2, 3]]) - background_list = program.vertex_list_indexed(bg_count, GL_TRIANGLES, background_indices, + background_list = decoration_program.vertex_list_indexed(bg_count * 4, GL_TRIANGLES, background_indices, layout.batch, layout.background_decoration_group, position=('f', background_vertices), colors=('Bn', background_colors)) context.add_list(background_list) if underline_vertices: - underline_list = program.vertex_list(len(underline_vertices) // 2, GL_LINES, + decoration_program = get_default_decoration_shader() + underline_list = decoration_program.vertex_list(len(underline_vertices) // 2, GL_LINES, layout.batch, layout.foreground_decoration_group, position=('f',underline_vertices), colors=('Bn', underline_colors)) @@ -778,6 +780,14 @@ class ScrollableTextDecorationGroup(graphics.Group): glDisable(GL_BLEND) self.program.stop() + def __repr__(self): + return f"{self.__class__.__name__}(scissor={self.scissor_area})" + + def __eq__(self, other): + return self is other + + def __hash__(self): + return id(self) class IncrementalTextDecorationGroup(ScrollableTextDecorationGroup): # Subclass so that the scissor_area isn't shared with the @@ -1797,6 +1807,9 @@ class ScrollableTextLayout(TextLayout): for group in self.group_cache.values(): group.scissor_area = area + self.background_decoration_group.scissor_area = area + self.foreground_decoration_group.scissor_area = area + def _update(self): super()._update() self._update_scissor_area() @@ -1951,6 +1964,8 @@ class IncrementalTextLayout(TextLayout, EventDispatcher): area = self._get_left(), self._get_bottom(self._get_lines()), self._width, self._height for group in self.group_cache.values(): group.scissor_area = area + self.background_decoration_group.scissor_area = area + self.foreground_decoration_group.scissor_area = area def _init_document(self): assert self._document, 'Cannot remove document from IncrementalTextLayout' @@ -2532,7 +2547,7 @@ class IncrementalTextLayout(TextLayout, EventDispatcher): position -= box.length x += box.advance - return x + self._translate_x, line.y + self._translate_y + baseline + return x - self._translate_x, line.y + self._translate_y + baseline def get_line_from_point(self, x, y): """Get the closest line index to a point. @@ -2610,6 +2625,8 @@ class IncrementalTextLayout(TextLayout, EventDispatcher): :rtype: int """ line = self.lines[line] + + x += self._translate_x x -= self._x if x < line.x: @@ -2659,6 +2676,8 @@ class IncrementalTextLayout(TextLayout, EventDispatcher): X coordinate """ + x += self.view_x - self._x + if x <= self.view_x + 10: self.view_x = x - 10 elif x >= self.view_x + self.width: diff --git a/libs/pyglet/window/win32/__init__.py b/libs/pyglet/window/win32/__init__.py index 58f7292..4158709 100644 --- a/libs/pyglet/window/win32/__init__.py +++ b/libs/pyglet/window/win32/__init__.py @@ -321,6 +321,7 @@ class Win32Window(BaseWindow): return _user32.DestroyWindow(self._hwnd) + _user32.UnregisterClassW(self._view_window_class.lpszClassName, 0) _user32.UnregisterClassW(self._window_class.lpszClassName, 0) self._window_class = None