From 61d3d0d477ede38133ba0a1d48f5d27305a82399 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 17 Apr 2024 18:18:36 -0400 Subject: [PATCH 01/31] Turn arcade.types into folder package --- arcade/{types.py => types/__init__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename arcade/{types.py => types/__init__.py} (100%) diff --git a/arcade/types.py b/arcade/types/__init__.py similarity index 100% rename from arcade/types.py rename to arcade/types/__init__.py From 124f5b85b70763b26fd05e76f91c390b488b9cdf Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 17 Apr 2024 20:00:39 -0400 Subject: [PATCH 02/31] Move color types/aliases to arcade.types.color --- arcade/types/__init__.py | 442 +------------------------------------ arcade/types/color.py | 459 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 470 insertions(+), 431 deletions(-) create mode 100644 arcade/types/color.py diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index b8e328fa47..ad44c7b41f 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -6,12 +6,10 @@ import sys from array import array import ctypes -import random from collections import namedtuple from collections.abc import ByteString from pathlib import Path from typing import ( - Iterable, List, NamedTuple, Optional, @@ -21,16 +19,9 @@ TYPE_CHECKING, TypeVar ) -from typing_extensions import Self from pytiled_parser import Properties -from arcade.utils import ( - IntOutsideRangeError, - ByteRangeError, - NormalizedRangeError -) - if TYPE_CHECKING: from arcade.texture import Texture @@ -39,29 +30,21 @@ #: 2. Tells readers we're converting any ints to floats AsFloat = Union[float, int] -MAX_UINT24 = 0xFFFFFF -MAX_UINT32 = 0xFFFFFFFF - -MASK_RGBA_R = 0xFF000000 -MASK_RGBA_G = 0x00FF0000 -MASK_RGBA_B = 0x0000FF00 -MASK_RGBA_A = 0x000000FF - -MASK_RGB_R = 0xFF0000 -MASK_RGB_G = 0x00FF00 -MASK_RGB_B = 0x0000FF -ChannelType = TypeVar('ChannelType') +# Generic color aliases +from arcade.types.color import RGB +from arcade.types.color import RGBA +from arcade.types.color import RGBOrA -RGB = Tuple[ChannelType, ChannelType, ChannelType] -RGBA = Tuple[ChannelType, ChannelType, ChannelType, ChannelType] -RGBOrA = Union[RGB[ChannelType], RGBA[ChannelType]] +# Specific color aliases +from arcade.types.color import RGBA255 +from arcade.types.color import RGBANormalized +from arcade.types.color import RGBOrA255 +from arcade.types.color import RGBOrANormalized -RGBOrA255 = RGBOrA[int] -RGBOrANormalized = RGBOrA[float] +# The Color helper type +from arcade.types.color import Color -RGBA255 = RGBA[int] -RGBANormalized = RGBA[float] RGBA255OrNormalized = Union[RGBA255, RGBANormalized] @@ -86,409 +69,6 @@ "Vector" ] - -class Color(RGBA255): - """ - A :py:class:`tuple` subclass representing an RGBA Color. - - This class provides helpful utility methods and properties. When - performance or brevity matters, arcade will usually allow you to - use an ordinary :py:class:`tuple` of RGBA values instead. - - All channels are byte values from 0 to 255, inclusive. If any are - outside this range, a :py:class:`~arcade.utils.ByteRangeError` will - be raised, which can be handled as a :py:class:`ValueError`. - - Examples:: - - >>> from arcade.types import Color - >>> Color(255, 0, 0) - Color(r=255, g=0, b=0, a=0) - - >>> Color(*rgb_green_tuple, 127) - Color(r=0, g=255, b=0, a=127) - - :param r: the red channel of the color, between 0 and 255 - :param g: the green channel of the color, between 0 and 255 - :param b: the blue channel of the color, between 0 and 255 - :param a: the alpha or transparency channel of the color, between - 0 and 255 - """ - - __slots__ = () - - def __new__(cls, r: int, g: int, b: int, a: int = 255): - - if not 0 <= r <= 255: - raise ByteRangeError("r", r) - - if not 0 <= g <= 255: - raise ByteRangeError("g", g) - - if not 0 <= g <= 255: - raise ByteRangeError("b", b) - - if not 0 <= a <= 255: - raise ByteRangeError("a", a) - - # Typechecking is ignored because of a mypy bug involving - # tuples & super: - # https://github.com/python/mypy/issues/8541 - return super().__new__(cls, (r, g, b, a)) # type: ignore - - def __deepcopy__(self, _) -> Self: - """Allow :py:func:`~copy.deepcopy` to be used with Color""" - return self.__class__(r=self.r, g=self.g, b=self.b, a=self.a) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(r={self.r}, g={self.g}, b={self.b}, a={self.a})" - - @property - def r(self) -> int: - return self[0] - - @property - def g(self) -> int: - return self[1] - - @property - def b(self) -> int: - return self[2] - - @property - def a(self) -> int: - return self[3] - - @property - def rgb(self) -> Tuple[int, int, int]: - """Return only a color's RGB components. - - This is syntactic sugar for slice indexing as below: - - .. code-block:: python - - >>> from arcade.color import WHITE - >>> WHITE[:3] - (255, 255, 255) - # Equivalent but slower than the above - >>> (WHITE.r, WHITE.g, WHITE.b) - (255, 255, 255) - - To reorder the channels as you retrieve them, see - :meth:`.swizzle`. - """ - return self[:3] - - @classmethod - def from_iterable(cls, iterable: Iterable[int]) -> Self: - """ - Create a color from an :py:class`Iterable` with 3-4 elements - - If the passed iterable is already a Color instance, it will be - returned unchanged. If the iterable has less than 3 or more than - 4 elements, a ValueError will be raised. - - Otherwise, the function will attempt to create a new Color - instance. The usual rules apply, ie all values must be between - 0 and 255, inclusive. - - :param iterable: An iterable which unpacks to 3 or 4 elements, - each between 0 and 255, inclusive. - """ - if isinstance(iterable, cls): - return iterable - - # We use unpacking because there isn't a good way of specifying - # lengths for sequences as of 3.8, our minimum Python version as - # of March 2023: https://github.com/python/typing/issues/786 - r, g, b, *_a = iterable - - if _a: - if len(_a) > 1: - raise ValueError("iterable must unpack to 3 or 4 values") - a = _a[0] - else: - a = 255 - - return cls(r, g, b, a=a) - - @property - def normalized(self) -> RGBANormalized: - """ - Return this color as a tuple of 4 normalized floats. - - Examples:: - - >>> arcade.color.WHITE.normalized - (1.0, 1.0, 1.0, 1.0) - - >>> arcade.color.BLACK.normalized - (0.0, 0.0, 0.0, 1.0) - - >>> arcade.color.TRANSPARENT_BLACK.normalized - (0.0, 0.0, 0.0, 0.0) - - """ - return self[0] / 255, self[1] / 255, self[2] / 255, self[3] / 255 - - @classmethod - def from_gray(cls, brightness: int, a: int = 255) -> Self: - """ - Return a shade of gray of the given brightness. - - Example:: - - >>> custom_white = Color.from_gray(255) - >>> print(custom_white) - Color(r=255, g=255, b=255, a=255) - - >>> half_opacity_gray = Color.from_gray(128, 128) - >>> print(half_opacity_gray) - Color(r=128, g=128, b=128, a=128) - - :param brightness: How bright the shade should be - :param a: a transparency value, fully opaque by default - :return: - """ - - if not 0 <= brightness <= 255: - raise ByteRangeError("brightness", brightness) - - if not 0 <= a <= 255: - raise ByteRangeError("a", a) - - return cls(brightness, brightness, brightness, a=a) - - @classmethod - def from_uint24(cls, color: int, a: int = 255) -> Self: - """ - Return a Color from an unsigned 3-byte (24 bit) integer. - - These ints may be between 0 and 16777215 (``0xFFFFFF``), inclusive. - - Example:: - - >>> Color.from_uint24(16777215) - Color(r=255, g=255, b=255, a=255) - - >>> Color.from_uint24(0xFF0000) - Color(r=255, g=0, b=0, a=255) - - :param color: a 3-byte int between 0 and 16777215 (``0xFFFFFF``) - :param a: an alpha value to use between 0 and 255, inclusive. - """ - - if not 0 <= color <= MAX_UINT24: - raise IntOutsideRangeError("color", color, 0, MAX_UINT24) - - if not 0 <= a <= 255: - raise ByteRangeError("a", a) - - return cls( - (color & 0xFF0000) >> 16, - (color & 0xFF00) >> 8, - color & 0xFF, - a=a - ) - - @classmethod - def from_uint32(cls, color: int) -> Self: - """ - Return a Color tuple for a given unsigned 4-byte (32-bit) integer - - The bytes are interpreted as R, G, B, A. - - Examples:: - - >>> Color.from_uint32(4294967295) - Color(r=255, g=255, b=255, a=255) - - >>> Color.from_uint32(0xFF0000FF) - Color(r=255, g=0, b=0, a=255) - - :param color: An int between 0 and 4294967295 (``0xFFFFFFFF``) - """ - if not 0 <= color <= MAX_UINT32: - raise IntOutsideRangeError("color", color, 0, MAX_UINT32) - - return cls( - (color & 0xFF000000) >> 24, - (color & 0xFF0000) >> 16, - (color & 0xFF00) >> 8, - a=(color & 0xFF) - ) - - @classmethod - def from_normalized(cls, color_normalized: RGBANormalized) -> Self: - """ - Convert normalized (0.0 to 1.0) channels into an RGBA Color - - If the input channels aren't normalized, a - :py:class:`arcade.utils.NormalizedRangeError` will be raised. - This is a subclass of :py:class`ValueError` and can be handled - as such. - - Examples:: - - >>> Color.from_normalized((1.0, 0.0, 0.0, 1.0)) - Color(r=255, g=0, b=0, a=255) - - >>> normalized_half_opacity_green = (0.0, 1.0, 0.0, 0.5) - >>> Color.from_normalized(normalized_half_opacity_green) - Color(r=0, g=255, b=0, a=127) - - :param color_normalized: The color as normalized (0.0 to 1.0) RGBA values. - :return: - """ - r, g, b, *_a = color_normalized - - if _a: - if len(_a) > 1: - raise ValueError("color_normalized must unpack to 3 or 4 values") - a = _a[0] - - if not 0.0 <= a <= 1.0: - raise NormalizedRangeError("a", a) - - else: - a = 1.0 - - if not 0.0 <= r <= 1.0: - raise NormalizedRangeError("r", r) - - if not 0.0 <= g <= 1.0: - raise NormalizedRangeError("g", g) - - if not 0.0 <= b <= 1.0: - raise NormalizedRangeError("b", b) - - return cls(int(255 * r), int(255 * g), int(255 * b), a=int(255 * a)) - - @classmethod - def from_hex_string(cls, code: str) -> Self: - """ - Make a color from a hex code that is 3, 4, 6, or 8 hex digits long - - Prefixing it with a pound sign (``#`` / hash symbol) is - optional. It will be ignored if present. - - The capitalization of the hex digits (``'f'`` vs ``'F'``) - does not matter. - - 3 and 6 digit hex codes will be treated as if they have an opacity of - 255. - - 3 and 4 digit hex codes will be expanded. - - Examples:: - - >>> Color.from_hex_string("#ff00ff") - Color(r=255, g=0, b=255, a=255) - - >>> Color.from_hex_string("#ff00ff00") - Color(r=255, g=0, b=255, a=0) - - >>> Color.from_hex_string("#FFF") - Color(r=255, g=255, b=255, a=255) - - >>> Color.from_hex_string("FF0A") - Color(r=255, g=255, b=0, a=170) - - """ - code = code.lstrip("#") - - # This looks unusual, but it matches CSS color code expansion - # behavior for 3 and 4 digit hex codes. - if len(code) <= 4: - code = "".join(char * 2 for char in code) - - if len(code) == 6: - # full opacity if no alpha specified - return cls(int(code[:2], 16), int(code[2:4], 16), int(code[4:6], 16), 255) - elif len(code) == 8: - return cls(int(code[:2], 16), int(code[2:4], 16), int(code[4:6], 16), int(code[6:8], 16)) - - raise ValueError(f"Improperly formatted color: '{code}'") - - @classmethod - def random( - cls, - r: Optional[int] = None, - g: Optional[int] = None, - b: Optional[int] = None, - a: Optional[int] = None, - ) -> Self: - """ - Return a random color. - - The parameters are optional and can be used to fix the value of - a particular channel. If a channel is not fixed, it will be - randomly generated. - - Examples:: - - # Randomize all channels - >>> Color.random() - Color(r=35, g=145, b=4, a=200) - - # Random color with fixed alpha - >>> Color.random(a=255) - Color(r=25, g=99, b=234, a=255) - - :param r: Fixed value for red channel - :param g: Fixed value for green channel - :param b: Fixed value for blue channel - :param a: Fixed value for alpha channel - """ - rand = random.randint(0, MAX_UINT32) - if r is None: - r = (rand & MASK_RGBA_R) >> 24 - if g is None: - g = (rand & MASK_RGBA_G) >> 16 - if b is None: - b = (rand & MASK_RGBA_B) >> 8 - if a is None: - a = (rand & MASK_RGBA_A) - - return cls(r, g, b, a) - - def swizzle(self, swizzle_string: str) -> Tuple[int, ...]: - """ - Get a tuple of channel values in the same order as the passed string. - - This imitates swizzling `as implemented in GLSL `_ - - .. code-block:: python - - >>> from arcade.types import Color - >>> color = Color(180, 90, 0, 255) - >>> color.swizzle("abgr") - (255, 0, 90, 180) - - You can also use any length of swizzle string and use capital - letters. Any capitals will be treated as lower case equivalents. - - .. code-block: python - - >>> from arcade.types import Color - >>> color = Color(180, 90, 0, 255) - >>> color.swizzle("ABGR") - (255, 0, 90, 180) - - - :param swizzle_string: - A string of channel names as letters in ``"RGBArgba"``. - :return: - A tuple in the same order as the input string. - """ - ret = [] - for c in swizzle_string.lower(): - if c not in 'rgba': - raise ValueError(f"Swizzle string must only contain characters in [RGBArgba], not {c}.") - ret.append(getattr(self, c)) - return tuple(ret) - - ColorLike = Union[RGB, RGBA255] # Point = Union[Tuple[float, float], List[float]] diff --git a/arcade/types/color.py b/arcade/types/color.py new file mode 100644 index 0000000000..2cade09df2 --- /dev/null +++ b/arcade/types/color.py @@ -0,0 +1,459 @@ +from __future__ import annotations + +import random +from typing import Tuple, Iterable, Optional, Union, TypeVar + +from typing_extensions import Self + +import arcade +from arcade.utils import ByteRangeError, IntOutsideRangeError, NormalizedRangeError + + +# Helpful color-related constants for bit masking +MAX_UINT24 = 0xFFFFFF +MAX_UINT32 = 0xFFFFFFFF +MASK_RGBA_R = 0xFF000000 +MASK_RGBA_G = 0x00FF0000 +MASK_RGBA_B = 0x0000FF00 +MASK_RGBA_A = 0x000000FF +MASK_RGB_R = 0xFF0000 +MASK_RGB_G = 0x00FF00 +MASK_RGB_B = 0x0000FF + + +# Color type aliases. +ChannelType = TypeVar('ChannelType') + +# Generic color aliases +RGB = Tuple[ChannelType, ChannelType, ChannelType] +RGBA = Tuple[ChannelType, ChannelType, ChannelType, ChannelType] +RGBOrA = Union[RGB[ChannelType], RGBA[ChannelType]] + +# Specific color aliases +RGBOrA255 = RGBOrA[int] +RGBOrANormalized = RGBOrA[float] +RGBA255 = RGBA[int] +RGBANormalized = RGBA[float] + + +class Color(RGBA255): + """ + A :py:class:`tuple` subclass representing an RGBA Color. + + This class provides helpful utility methods and properties. When + performance or brevity matters, arcade will usually allow you to + use an ordinary :py:class:`tuple` of RGBA values instead. + + All channels are byte values from 0 to 255, inclusive. If any are + outside this range, a :py:class:`~arcade.utils.ByteRangeError` will + be raised, which can be handled as a :py:class:`ValueError`. + + Examples:: + + >>> from arcade.types import Color + >>> Color(255, 0, 0) + Color(r=255, g=0, b=0, a=0) + + >>> Color(*rgb_green_tuple, 127) + Color(r=0, g=255, b=0, a=127) + + :param r: the red channel of the color, between 0 and 255 + :param g: the green channel of the color, between 0 and 255 + :param b: the blue channel of the color, between 0 and 255 + :param a: the alpha or transparency channel of the color, between + 0 and 255 + """ + + __slots__ = () + + def __new__(cls, r: int, g: int, b: int, a: int = 255): + + if not 0 <= r <= 255: + raise ByteRangeError("r", r) + + if not 0 <= g <= 255: + raise ByteRangeError("g", g) + + if not 0 <= g <= 255: + raise ByteRangeError("b", b) + + if not 0 <= a <= 255: + raise ByteRangeError("a", a) + + # Typechecking is ignored because of a mypy bug involving + # tuples & super: + # https://github.com/python/mypy/issues/8541 + return super().__new__(cls, (r, g, b, a)) # type: ignore + + def __deepcopy__(self, _) -> Self: + """Allow :py:func:`~copy.deepcopy` to be used with Color""" + return self.__class__(r=self.r, g=self.g, b=self.b, a=self.a) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(r={self.r}, g={self.g}, b={self.b}, a={self.a})" + + @property + def r(self) -> int: + return self[0] + + @property + def g(self) -> int: + return self[1] + + @property + def b(self) -> int: + return self[2] + + @property + def a(self) -> int: + return self[3] + + @property + def rgb(self) -> Tuple[int, int, int]: + """Return only a color's RGB components. + + This is syntactic sugar for slice indexing as below: + + .. code-block:: python + + >>> from arcade.color import WHITE + >>> WHITE[:3] + (255, 255, 255) + # Equivalent but slower than the above + >>> (WHITE.r, WHITE.g, WHITE.b) + (255, 255, 255) + + To reorder the channels as you retrieve them, see + :meth:`.swizzle`. + """ + return self[:3] + + @classmethod + def from_iterable(cls, iterable: Iterable[int]) -> Self: + """ + Create a color from an :py:class`Iterable` with 3-4 elements + + If the passed iterable is already a Color instance, it will be + returned unchanged. If the iterable has less than 3 or more than + 4 elements, a ValueError will be raised. + + Otherwise, the function will attempt to create a new Color + instance. The usual rules apply, ie all values must be between + 0 and 255, inclusive. + + :param iterable: An iterable which unpacks to 3 or 4 elements, + each between 0 and 255, inclusive. + """ + if isinstance(iterable, cls): + return iterable + + # We use unpacking because there isn't a good way of specifying + # lengths for sequences as of 3.8, our minimum Python version as + # of March 2023: https://github.com/python/typing/issues/786 + r, g, b, *_a = iterable + + if _a: + if len(_a) > 1: + raise ValueError("iterable must unpack to 3 or 4 values") + a = _a[0] + else: + a = 255 + + return cls(r, g, b, a=a) + + @property + def normalized(self) -> RGBANormalized: + """ + Return this color as a tuple of 4 normalized floats. + + Examples:: + + >>> arcade.color.WHITE.normalized + (1.0, 1.0, 1.0, 1.0) + + >>> arcade.color.BLACK.normalized + (0.0, 0.0, 0.0, 1.0) + + >>> arcade.color.TRANSPARENT_BLACK.normalized + (0.0, 0.0, 0.0, 0.0) + + """ + return self[0] / 255, self[1] / 255, self[2] / 255, self[3] / 255 + + @classmethod + def from_gray(cls, brightness: int, a: int = 255) -> Self: + """ + Return a shade of gray of the given brightness. + + Example:: + + >>> custom_white = Color.from_gray(255) + >>> print(custom_white) + Color(r=255, g=255, b=255, a=255) + + >>> half_opacity_gray = Color.from_gray(128, 128) + >>> print(half_opacity_gray) + Color(r=128, g=128, b=128, a=128) + + :param brightness: How bright the shade should be + :param a: a transparency value, fully opaque by default + :return: + """ + + if not 0 <= brightness <= 255: + raise ByteRangeError("brightness", brightness) + + if not 0 <= a <= 255: + raise ByteRangeError("a", a) + + return cls(brightness, brightness, brightness, a=a) + + @classmethod + def from_uint24(cls, color: int, a: int = 255) -> Self: + """ + Return a Color from an unsigned 3-byte (24 bit) integer. + + These ints may be between 0 and 16777215 (``0xFFFFFF``), inclusive. + + Example:: + + >>> Color.from_uint24(16777215) + Color(r=255, g=255, b=255, a=255) + + >>> Color.from_uint24(0xFF0000) + Color(r=255, g=0, b=0, a=255) + + :param color: a 3-byte int between 0 and 16777215 (``0xFFFFFF``) + :param a: an alpha value to use between 0 and 255, inclusive. + """ + + if not 0 <= color <= MAX_UINT24: + raise IntOutsideRangeError("color", color, 0, MAX_UINT24) + + if not 0 <= a <= 255: + raise ByteRangeError("a", a) + + return cls( + (color & 0xFF0000) >> 16, + (color & 0xFF00) >> 8, + color & 0xFF, + a=a + ) + + @classmethod + def from_uint32(cls, color: int) -> Self: + """ + Return a Color tuple for a given unsigned 4-byte (32-bit) integer + + The bytes are interpreted as R, G, B, A. + + Examples:: + + >>> Color.from_uint32(4294967295) + Color(r=255, g=255, b=255, a=255) + + >>> Color.from_uint32(0xFF0000FF) + Color(r=255, g=0, b=0, a=255) + + :param color: An int between 0 and 4294967295 (``0xFFFFFFFF``) + """ + if not 0 <= color <= MAX_UINT32: + raise IntOutsideRangeError("color", color, 0, MAX_UINT32) + + return cls( + (color & 0xFF000000) >> 24, + (color & 0xFF0000) >> 16, + (color & 0xFF00) >> 8, + a=(color & 0xFF) + ) + + @classmethod + def from_normalized(cls, color_normalized: RGBANormalized) -> Self: + """ + Convert normalized (0.0 to 1.0) channels into an RGBA Color + + If the input channels aren't normalized, a + :py:class:`arcade.utils.NormalizedRangeError` will be raised. + This is a subclass of :py:class`ValueError` and can be handled + as such. + + Examples:: + + >>> Color.from_normalized((1.0, 0.0, 0.0, 1.0)) + Color(r=255, g=0, b=0, a=255) + + >>> normalized_half_opacity_green = (0.0, 1.0, 0.0, 0.5) + >>> Color.from_normalized(normalized_half_opacity_green) + Color(r=0, g=255, b=0, a=127) + + :param color_normalized: The color as normalized (0.0 to 1.0) RGBA values. + :return: + """ + r, g, b, *_a = color_normalized + + if _a: + if len(_a) > 1: + raise ValueError("color_normalized must unpack to 3 or 4 values") + a = _a[0] + + if not 0.0 <= a <= 1.0: + raise NormalizedRangeError("a", a) + + else: + a = 1.0 + + if not 0.0 <= r <= 1.0: + raise NormalizedRangeError("r", r) + + if not 0.0 <= g <= 1.0: + raise NormalizedRangeError("g", g) + + if not 0.0 <= b <= 1.0: + raise NormalizedRangeError("b", b) + + return cls(int(255 * r), int(255 * g), int(255 * b), a=int(255 * a)) + + @classmethod + def from_hex_string(cls, code: str) -> Self: + """ + Make a color from a hex code that is 3, 4, 6, or 8 hex digits long + + Prefixing it with a pound sign (``#`` / hash symbol) is + optional. It will be ignored if present. + + The capitalization of the hex digits (``'f'`` vs ``'F'``) + does not matter. + + 3 and 6 digit hex codes will be treated as if they have an opacity of + 255. + + 3 and 4 digit hex codes will be expanded. + + Examples:: + + >>> Color.from_hex_string("#ff00ff") + Color(r=255, g=0, b=255, a=255) + + >>> Color.from_hex_string("#ff00ff00") + Color(r=255, g=0, b=255, a=0) + + >>> Color.from_hex_string("#FFF") + Color(r=255, g=255, b=255, a=255) + + >>> Color.from_hex_string("FF0A") + Color(r=255, g=255, b=0, a=170) + + """ + code = code.lstrip("#") + + # This looks unusual, but it matches CSS color code expansion + # behavior for 3 and 4 digit hex codes. + if len(code) <= 4: + code = "".join(char * 2 for char in code) + + if len(code) == 6: + # full opacity if no alpha specified + return cls(int(code[:2], 16), int(code[2:4], 16), int(code[4:6], 16), 255) + elif len(code) == 8: + return cls(int(code[:2], 16), int(code[2:4], 16), int(code[4:6], 16), int(code[6:8], 16)) + + raise ValueError(f"Improperly formatted color: '{code}'") + + @classmethod + def random( + cls, + r: Optional[int] = None, + g: Optional[int] = None, + b: Optional[int] = None, + a: Optional[int] = None, + ) -> Self: + """ + Return a random color. + + The parameters are optional and can be used to fix the value of + a particular channel. If a channel is not fixed, it will be + randomly generated. + + Examples:: + + # Randomize all channels + >>> Color.random() + Color(r=35, g=145, b=4, a=200) + + # Random color with fixed alpha + >>> Color.random(a=255) + Color(r=25, g=99, b=234, a=255) + + :param r: Fixed value for red channel + :param g: Fixed value for green channel + :param b: Fixed value for blue channel + :param a: Fixed value for alpha channel + """ + rand = random.randint(0, MAX_UINT32) + if r is None: + r = (rand & MASK_RGBA_R) >> 24 + if g is None: + g = (rand & MASK_RGBA_G) >> 16 + if b is None: + b = (rand & MASK_RGBA_B) >> 8 + if a is None: + a = (rand & MASK_RGBA_A) + + return cls(r, g, b, a) + + def swizzle(self, swizzle_string: str) -> Tuple[int, ...]: + """ + Get a tuple of channel values in the same order as the passed string. + + This imitates swizzling `as implemented in GLSL `_ + + .. code-block:: python + + >>> from arcade.types import Color + >>> color = Color(180, 90, 0, 255) + >>> color.swizzle("abgr") + (255, 0, 90, 180) + + You can also use any length of swizzle string and use capital + letters. Any capitals will be treated as lower case equivalents. + + .. code-block: python + + >>> from arcade.types import Color + >>> color = Color(180, 90, 0, 255) + >>> color.swizzle("ABGR") + (255, 0, 90, 180) + + + :param swizzle_string: + A string of channel names as letters in ``"RGBArgba"``. + :return: + A tuple in the same order as the input string. + """ + ret = [] + for c in swizzle_string.lower(): + if c not in 'rgba': + raise ValueError(f"Swizzle string must only contain characters in [RGBArgba], not {c}.") + ret.append(getattr(self, c)) + return tuple(ret) + + +__all__ = ( + 'Color', + 'RGB', + 'RGBA', + 'RGBA255', + 'RGBANormalized', + 'RGBOrA', + 'RGBOrA255', + 'RGBOrANormalized', + 'MASK_RGBA_R', + 'MASK_RGBA_G', + 'MASK_RGBA_B', + 'MASK_RGBA_A', + 'MASK_RGB_R', + 'MASK_RGB_G', + 'MASK_RGB_B', + 'MAX_UINT24', + 'MAX_UINT32', +) From 71d242b26b4295e9c72b5d188873dc5176322a16 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 17 Apr 2024 20:04:53 -0400 Subject: [PATCH 03/31] Remove unused RGBAOrA255OrNormalized --- arcade/types/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index ad44c7b41f..44af288c87 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -46,9 +46,6 @@ from arcade.types.color import Color -RGBA255OrNormalized = Union[RGBA255, RGBANormalized] - - __all__ = [ "BufferProtocol", "Color", @@ -64,7 +61,6 @@ "RGB", "RGBA255", "RGBANormalized", - "RGBA255OrNormalized", "TiledObject", "Vector" ] From 25ec5b40c143ce691656e40f9690b17f46ea0a3f Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 17 Apr 2024 20:20:31 -0400 Subject: [PATCH 04/31] Clean up arcade.types imports * Remove unused TypeVar import * Move BufferProtocol conditional import to the top * Make backport-related imports conditional instead of universal --- arcade/types/__init__.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 44af288c87..33f26a8838 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -4,10 +4,7 @@ from __future__ import annotations import sys -from array import array -import ctypes from collections import namedtuple -from collections.abc import ByteString from pathlib import Path from typing import ( List, @@ -17,11 +14,28 @@ Tuple, Union, TYPE_CHECKING, - TypeVar ) from pytiled_parser import Properties + +# Backward-compatibility for buffer protocol objects +# To learn more, see https://docs.python.org/3.8/c-api/buffer.html +if sys.version_info >= (3, 12): + from collections.abc import Buffer as BufferProtocol +else: + # The most common built-in buffer protocol objects + import ctypes + from array import array + from collections.abc import ByteString + + # This is used instead of the typing_extensions version since they + # use an ABC which registers virtual subclasses. This will not work + # with ctypes.Array since virtual subclasses must be concrete. + # See: https://peps.python.org/pep-0688/ + BufferProtocol = Union[ByteString, memoryview, array, ctypes.Array] + + if TYPE_CHECKING: from arcade.texture import Texture @@ -97,11 +111,3 @@ class TiledObject(NamedTuple): type: Optional[str] = None -if sys.version_info >= (3, 12): - from collections.abc import Buffer as BufferProtocol -else: - # This is used instead of the typing_extensions version since they - # use an ABC which registers virtual subclasses. This will not work - # with ctypes.Array since virtual subclasses must be concrete. - # See: https://peps.python.org/pep-0688/ - BufferProtocol = Union[ByteString, memoryview, array, ctypes.Array] From d328edb7b9037a59283307a72adfb6df4e52701a Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 17 Apr 2024 20:25:24 -0400 Subject: [PATCH 05/31] Remove redundant NamedPoint named tuple --- arcade/experimental/shapes_perf.py | 5 ++--- arcade/types/__init__.py | 3 --- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/arcade/experimental/shapes_perf.py b/arcade/experimental/shapes_perf.py index f29b35bc74..86af9a3fce 100644 --- a/arcade/experimental/shapes_perf.py +++ b/arcade/experimental/shapes_perf.py @@ -8,7 +8,6 @@ import random import arcade -from arcade.types import NamedPoint from pyglet.math import Mat4 SCREEN_WIDTH = 800 @@ -93,12 +92,12 @@ def __init__(self, width, height, title): # line strip self.line_strip = [ - NamedPoint(random.randint(0, SCREEN_WIDTH), random.randint(0, SCREEN_HEIGHT)) + (random.randint(0, SCREEN_WIDTH), random.randint(0, SCREEN_HEIGHT)) for _ in range(10) ] # Random list of points self.points = [ - NamedPoint(random.randint(0, SCREEN_WIDTH), random.randint(0, SCREEN_HEIGHT)) + (random.randint(0, SCREEN_WIDTH), random.randint(0, SCREEN_HEIGHT)) for _ in range(10_000) ] diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 33f26a8838..a79b3bfe11 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations import sys -from collections import namedtuple from pathlib import Path from typing import ( List, @@ -69,7 +68,6 @@ "Point", "PointList", "EMPTY_POINT_LIST", - "NamedPoint", "Rect", "RectList", "RGB", @@ -87,7 +85,6 @@ Point3 = Tuple[float, float, float] IPoint = Tuple[int, int] Vector = Point -NamedPoint = namedtuple("NamedPoint", ["x", "y"]) PointList = Sequence[Point] From a3dc13cf12c71fcbd485e1e0e68271b594021a54 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 17 Apr 2024 21:39:19 -0400 Subject: [PATCH 06/31] Simplify V_2D and V_3D in arcade.math * Remove Vec2 and Vec3 from the unions since pyglet's Vec tpyes are now tuples * Remove Vec2 and Vec3 imports from arcade.math --- arcade/math.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/arcade/math.py b/arcade/math.py index 636cafb6e5..b4326857b9 100644 --- a/arcade/math.py +++ b/arcade/math.py @@ -3,7 +3,6 @@ import math import random from typing import Tuple, List, Union -from pyglet.math import Vec2, Vec3 from arcade.types import Point, Vector _PRECISION = 2 @@ -63,15 +62,15 @@ def clamp(a, low: float, high: float) -> float: return high if a > high else max(a, low) +V_2D = Union[Tuple[float, float], List[float]] +V_3D = Union[Tuple[float, float, float], List[float]] + + def lerp(v1: float, v2: float, u: float) -> float: """linearly interpolate between two values""" return v1 + ((v2 - v1) * u) -V_2D = Union[Vec2, Tuple[float, float], List[float]] -V_3D = Union[Vec3, Tuple[float, float, float], List[float]] - - def lerp_2d(v1: V_2D, v2: V_2D, u: float) -> Tuple[float, float]: return ( lerp(v1[0], v2[0], u), From fc9a708ceddfec71584fad0ad6fa89dc0935b83b Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 17 Apr 2024 22:11:06 -0400 Subject: [PATCH 07/31] Remove Vector alias with 2 uses --- arcade/math.py | 6 +++--- arcade/types/__init__.py | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/arcade/math.py b/arcade/math.py index b4326857b9..2911b71396 100644 --- a/arcade/math.py +++ b/arcade/math.py @@ -3,7 +3,7 @@ import math import random from typing import Tuple, List, Union -from arcade.types import Point, Vector +from arcade.types import Point _PRECISION = 2 @@ -201,7 +201,7 @@ def rand_vec_spread_deg( angle: float, half_angle_spread: float, length: float -) -> Vector: +) -> tuple[float, float]: """ Returns a random vector, within a spread of the given angle. @@ -219,7 +219,7 @@ def rand_vec_magnitude( angle: float, lo_magnitude: float, hi_magnitude: float, -) -> Vector: +) -> tuple[float, float]: """ Returns a random vector, within a spread of the given angle. diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index a79b3bfe11..f39ea10b04 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -74,17 +74,14 @@ "RGBA255", "RGBANormalized", "TiledObject", - "Vector" ] ColorLike = Union[RGB, RGBA255] # Point = Union[Tuple[float, float], List[float]] -# Vector = Point Point = Tuple[float, float] Point3 = Tuple[float, float, float] IPoint = Tuple[int, int] -Vector = Point PointList = Sequence[Point] From 3f8c0e771cdb4456ee0cbb82ee21d7cc0f99cf72 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 17 Apr 2024 22:12:31 -0400 Subject: [PATCH 08/31] Remove currently broken and useless ColorLike type --- arcade/types/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index f39ea10b04..65268870a7 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -62,7 +62,6 @@ __all__ = [ "BufferProtocol", "Color", - "ColorLike", "IPoint", "PathOrTexture", "Point", @@ -76,7 +75,6 @@ "TiledObject", ] -ColorLike = Union[RGB, RGBA255] # Point = Union[Tuple[float, float], List[float]] Point = Tuple[float, float] From 19c9eb47bdc336bca70c89be5f66ffc580c60303 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 17 Apr 2024 22:44:22 -0400 Subject: [PATCH 09/31] Broaden typing in V_2D, V_3D, and lerp --- arcade/math.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/arcade/math.py b/arcade/math.py index 2911b71396..8e9fa1b911 100644 --- a/arcade/math.py +++ b/arcade/math.py @@ -2,8 +2,9 @@ import math import random -from typing import Tuple, List, Union -from arcade.types import Point +from typing import Sequence, Tuple, Union +from arcade.types import AsFloat, Point + _PRECISION = 2 @@ -62,11 +63,11 @@ def clamp(a, low: float, high: float) -> float: return high if a > high else max(a, low) -V_2D = Union[Tuple[float, float], List[float]] -V_3D = Union[Tuple[float, float, float], List[float]] +V_2D = Union[Tuple[AsFloat, AsFloat], Sequence[AsFloat]] +V_3D = Union[Tuple[AsFloat, AsFloat, AsFloat], Sequence[AsFloat]] -def lerp(v1: float, v2: float, u: float) -> float: +def lerp(v1: AsFloat, v2: AsFloat, u: float) -> float: """linearly interpolate between two values""" return v1 + ((v2 - v1) * u) From ce129ac28e53e9704e5004cad429a7d166ed6dee Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 17 Apr 2024 22:44:59 -0400 Subject: [PATCH 10/31] Add AsFloat to arcade.types.__all__ --- arcade/types/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 65268870a7..9131249a6f 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -60,6 +60,7 @@ __all__ = [ + "AsFloat", "BufferProtocol", "Color", "IPoint", From 328bf99c2ef97ded7daa6d4d01022ba1e39961f9 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 17 Apr 2024 22:49:12 -0400 Subject: [PATCH 11/31] Clean up Path-related annotations * Add subscriptable PathOr alias * Remove Optional from PathOrTexture * Wrap usages of PathOrTexture in Optional * Add bytes to PathLike Union alias * Clean up imports in some PathOrTexture-using files * Move the if TYPE_CHECKING import of Texture closer to the usage --- arcade/examples/particle_fireworks.py | 7 ++++--- arcade/particles/particle.py | 9 ++++----- arcade/sprite/sprite.py | 2 +- arcade/types/__init__.py | 18 ++++++++++++++---- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/arcade/examples/particle_fireworks.py b/arcade/examples/particle_fireworks.py index 361e833770..6ae4bda3c9 100644 --- a/arcade/examples/particle_fireworks.py +++ b/arcade/examples/particle_fireworks.py @@ -7,13 +7,14 @@ python -m arcade.examples.particle_fireworks """ import random +from typing import Optional + import pyglet from pyglet.math import Vec2 import arcade -from arcade.types import Point +from arcade.types import Point, PathOrTexture from arcade.math import rand_in_rect, clamp, lerp, rand_in_circle, rand_on_circle -from arcade.types import PathOrTexture from arcade.particles import ( Emitter, LifetimeParticle, @@ -127,7 +128,7 @@ class AnimatedAlphaParticle(LifetimeParticle): def __init__( self, - filename_or_texture: PathOrTexture, + filename_or_texture: Optional[PathOrTexture], change_xy: Vec2, start_alpha: int = 0, duration1: float = 1.0, diff --git a/arcade/particles/particle.py b/arcade/particles/particle.py index 9cc3bcba7d..bf3a42a683 100644 --- a/arcade/particles/particle.py +++ b/arcade/particles/particle.py @@ -2,12 +2,11 @@ Particle - Object produced by an Emitter. Often used in large quantity to produce visual effects effects """ from __future__ import annotations -from typing import Literal +from typing import Literal, Optional, Tuple from arcade.sprite import Sprite from arcade.math import lerp, clamp -from arcade.types import Point, Vector -from arcade.types import PathOrTexture +from arcade.types import Point, PathOrTexture class Particle(Sprite): @@ -15,8 +14,8 @@ class Particle(Sprite): def __init__( self, - path_or_texture: PathOrTexture, - change_xy: Vector, + path_or_texture: Optional[PathOrTexture], + change_xy: Tuple[float, float], center_xy: Point = (0.0, 0.0), angle: float = 0.0, change_angle: float = 0.0, diff --git a/arcade/sprite/sprite.py b/arcade/sprite/sprite.py index ce4e30f2ce..944d365d7f 100644 --- a/arcade/sprite/sprite.py +++ b/arcade/sprite/sprite.py @@ -64,7 +64,7 @@ class Sprite(BasicSprite, PymunkMixin): def __init__( self, - path_or_texture: PathOrTexture = None, + path_or_texture: Optional[PathOrTexture] = None, scale: float = 1.0, center_x: float = 0.0, center_y: float = 0.0, diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 9131249a6f..f74f18e8fa 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -13,6 +13,7 @@ Tuple, Union, TYPE_CHECKING, + TypeVar ) from pytiled_parser import Properties @@ -35,9 +36,6 @@ BufferProtocol = Union[ByteString, memoryview, array, ctypes.Array] -if TYPE_CHECKING: - from arcade.texture import Texture - #: 1. Makes pyright happier while also telling readers #: 2. Tells readers we're converting any ints to floats @@ -64,6 +62,7 @@ "BufferProtocol", "Color", "IPoint", + "PathOr", "PathOrTexture", "Point", "PointList", @@ -94,7 +93,18 @@ RectList = Union[Tuple[Rect, ...], List[Rect]] FloatRect = Union[Tuple[float, float, float, float], List[float]] # x, y, width, height -PathOrTexture = Optional[Union[str, Path, "Texture"]] + +# Path handling +PathLike = Union[str, Path, bytes] +_POr = TypeVar('_POr') # Allows PathOr[TypeNameHere] syntax +PathOr = Union[PathLike, _POr] + + +# Specific utility resource aliases with type imports +if TYPE_CHECKING: + from arcade.texture import Texture + +PathOrTexture = PathOr["Texture"] class TiledObject(NamedTuple): From 2e10d63c78ea1a0f6bfddd8bb73be811a54d19de Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 17 Apr 2024 22:53:32 -0400 Subject: [PATCH 12/31] Clean up whitespace in arcade/types/__init__.py --- arcade/types/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index f74f18e8fa..04c540952c 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -36,7 +36,6 @@ BufferProtocol = Union[ByteString, memoryview, array, ctypes.Array] - #: 1. Makes pyright happier while also telling readers #: 2. Tells readers we're converting any ints to floats AsFloat = Union[float, int] @@ -112,5 +111,3 @@ class TiledObject(NamedTuple): properties: Optional[Properties] = None name: Optional[str] = None type: Optional[str] = None - - From 676d104340fe3c1937cbe484615bcf6913c0126a Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 17 Apr 2024 22:55:35 -0400 Subject: [PATCH 13/31] Add top-level docstring for arcade.types.color --- arcade/types/color.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/arcade/types/color.py b/arcade/types/color.py index 2cade09df2..ab66a227b2 100644 --- a/arcade/types/color.py +++ b/arcade/types/color.py @@ -1,3 +1,22 @@ +"""Color-related types, aliases, and constants. + +This module does not contain pre-defined color values. For pre-made +named color values, please see the following: + +.. list-table:: + :header-rows: 1 + + * - Module + - Contents + + * - :py:mod:`arcade.color` + - A set of pre-defined :py:class`.Color` constants. + + * - :py:mod:`arcade.csscolor` + - The `CSS named colors `_ + as :py:class:`.Color` constants. + +""" from __future__ import annotations import random From 6bd84a128f8035634171e72dbc6efd7abab6aec9 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 17 Apr 2024 23:39:13 -0400 Subject: [PATCH 14/31] Add top-level docstring for arcade.types --- arcade/types/__init__.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 04c540952c..68ef5c6d2c 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -1,5 +1,23 @@ -""" -Module specifying data custom types used for type hinting. +"""Fundamental aliases, classes, and related constants. + +As general rules: + +#. Things only go in this module if they serve multiple purposes + throughout arcade +#. Only expose the most important classes at this module's top level + +For example, color-related types and related aliases go in +``arcade.types`` because they're used throughout the codebase. This +includes all the following areas: + +#. :py:class:`~arcade.Sprite` +#. :py:class:`~arcade.SpriteList` +#. :py:class:`~arcade.Text` +#. The :py:mod:`arcade.gui` widgets +#. Functions in :py:mod:`arcade.drawing_commands` + +However, since the color types, aliases, and constants are all related, +they go in the :py:mod:`arcade.types.color` submodule. """ from __future__ import annotations From 6f35f9f0870f868eaf2bd914d3a23c139d421aa1 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 05:38:56 -0400 Subject: [PATCH 15/31] Use AsFloat in current stubbed Rect annotations --- arcade/types/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 68ef5c6d2c..cfa6190e5c 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -93,9 +93,9 @@ ] -# Point = Union[Tuple[float, float], List[float]] -Point = Tuple[float, float] -Point3 = Tuple[float, float, float] +# Point = Union[Tuple[AsFloat, AsFloat], List[AsFloat]] +Point = Tuple[AsFloat, AsFloat] +Point3 = Tuple[AsFloat, AsFloat, AsFloat] IPoint = Tuple[int, int] @@ -108,7 +108,7 @@ Rect = Union[Tuple[int, int, int, int], List[int]] # x, y, width, height RectList = Union[Tuple[Rect, ...], List[Rect]] -FloatRect = Union[Tuple[float, float, float, float], List[float]] # x, y, width, height +FloatRect = Union[Tuple[AsFloat, AsFloat, AsFloat, AsFloat], List[AsFloat]] # x, y, width, height # Path handling From fe071fd04b81d9b0af8a99f1c356da5cce0a2c31 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 05:43:16 -0400 Subject: [PATCH 16/31] Remove unused import --- arcade/types/color.py | 1 - 1 file changed, 1 deletion(-) diff --git a/arcade/types/color.py b/arcade/types/color.py index ab66a227b2..34c1ebf9f7 100644 --- a/arcade/types/color.py +++ b/arcade/types/color.py @@ -24,7 +24,6 @@ from typing_extensions import Self -import arcade from arcade.utils import ByteRangeError, IntOutsideRangeError, NormalizedRangeError From 26aa06c5c1140d144e1c59905935646b2a1271ad Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 05:49:28 -0400 Subject: [PATCH 17/31] Move __all__ to top of arcade.types.color --- arcade/types/color.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/arcade/types/color.py b/arcade/types/color.py index 34c1ebf9f7..4282326ab5 100644 --- a/arcade/types/color.py +++ b/arcade/types/color.py @@ -27,6 +27,27 @@ from arcade.utils import ByteRangeError, IntOutsideRangeError, NormalizedRangeError +__all__ = ( + 'Color', + 'RGB', + 'RGBA', + 'RGBA255', + 'RGBANormalized', + 'RGBOrA', + 'RGBOrA255', + 'RGBOrANormalized', + 'MASK_RGBA_R', + 'MASK_RGBA_G', + 'MASK_RGBA_B', + 'MASK_RGBA_A', + 'MASK_RGB_R', + 'MASK_RGB_G', + 'MASK_RGB_B', + 'MAX_UINT24', + 'MAX_UINT32', +) + + # Helpful color-related constants for bit masking MAX_UINT24 = 0xFFFFFF MAX_UINT32 = 0xFFFFFFFF @@ -454,24 +475,3 @@ def swizzle(self, swizzle_string: str) -> Tuple[int, ...]: raise ValueError(f"Swizzle string must only contain characters in [RGBArgba], not {c}.") ret.append(getattr(self, c)) return tuple(ret) - - -__all__ = ( - 'Color', - 'RGB', - 'RGBA', - 'RGBA255', - 'RGBANormalized', - 'RGBOrA', - 'RGBOrA255', - 'RGBOrANormalized', - 'MASK_RGBA_R', - 'MASK_RGBA_G', - 'MASK_RGBA_B', - 'MASK_RGBA_A', - 'MASK_RGB_R', - 'MASK_RGB_G', - 'MASK_RGB_B', - 'MAX_UINT24', - 'MAX_UINT32', -) From 60654e6e96ed77c64c7f04dcee7e191079dec79d Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 05:58:10 -0400 Subject: [PATCH 18/31] Try to fix imports / __all__ --- arcade/types/__init__.py | 6 ++++++ arcade/types/color.py | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index cfa6190e5c..3a30076aa6 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -65,7 +65,9 @@ from arcade.types.color import RGBOrA # Specific color aliases +from arcade.types.color import RGB255 from arcade.types.color import RGBA255 +from arcade.types.color import RGBNormalized from arcade.types.color import RGBANormalized from arcade.types.color import RGBOrA255 from arcade.types.color import RGBOrANormalized @@ -87,7 +89,11 @@ "Rect", "RectList", "RGB", + "RGBOrA", "RGBA255", + "RGB255", + "RGBNormalized", + "RGBOrA255", "RGBANormalized", "TiledObject", ] diff --git a/arcade/types/color.py b/arcade/types/color.py index 4282326ab5..9369535b88 100644 --- a/arcade/types/color.py +++ b/arcade/types/color.py @@ -31,7 +31,9 @@ 'Color', 'RGB', 'RGBA', + 'RGB255', 'RGBA255', + 'RGBNormalized', 'RGBANormalized', 'RGBOrA', 'RGBOrA255', @@ -69,10 +71,12 @@ RGBOrA = Union[RGB[ChannelType], RGBA[ChannelType]] # Specific color aliases -RGBOrA255 = RGBOrA[int] -RGBOrANormalized = RGBOrA[float] +RGB255 = RGB[int] RGBA255 = RGBA[int] +RGBNormalized = RGB[float] RGBANormalized = RGBA[float] +RGBOrA255 = RGBOrA[int] +RGBOrANormalized = RGBOrA[float] class Color(RGBA255): From 2b2dd4597606f7b1fa2130652fdf710012d7f1c5 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 06:42:56 -0400 Subject: [PATCH 19/31] Defer total Vector replacement until later PR * Add back Vector alias * Rename it to Velocity in arcade.types * Change imports in particles + annotations in particles to account for this --- arcade/particles/emitter.py | 4 ++-- arcade/particles/particle.py | 8 ++++---- arcade/types/__init__.py | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/arcade/particles/emitter.py b/arcade/particles/emitter.py index 67f7e849ad..60475960b8 100644 --- a/arcade/particles/emitter.py +++ b/arcade/particles/emitter.py @@ -8,7 +8,7 @@ from .particle import Particle from typing import Optional, Callable, cast from arcade.math import _Vec2 -from arcade.types import Point, Vector +from arcade.types import Point, Velocity class EmitController: @@ -110,7 +110,7 @@ def __init__( center_xy: Point, emit_controller: EmitController, particle_factory: Callable[["Emitter"], Particle], - change_xy: Vector = (0.0, 0.0), + change_xy: Velocity = (0.0, 0.0), emit_done_cb: Optional[Callable[["Emitter"], None]] = None, reap_cb: Optional[Callable[[], None]] = None ): diff --git a/arcade/particles/particle.py b/arcade/particles/particle.py index bf3a42a683..f55cc02f19 100644 --- a/arcade/particles/particle.py +++ b/arcade/particles/particle.py @@ -6,7 +6,7 @@ from arcade.sprite import Sprite from arcade.math import lerp, clamp -from arcade.types import Point, PathOrTexture +from arcade.types import Point, PathOrTexture, Velocity class Particle(Sprite): @@ -53,7 +53,7 @@ class EternalParticle(Particle): def __init__( self, filename_or_texture: PathOrTexture, - change_xy: Vector, + change_xy: Velocity, center_xy: Point = (0.0, 0.0), angle: float = 0, change_angle: float = 0, @@ -75,7 +75,7 @@ class LifetimeParticle(Particle): def __init__( self, filename_or_texture: PathOrTexture, - change_xy: Vector, + change_xy: Velocity, lifetime: float, center_xy: Point = (0.0, 0.0), angle: float = 0, @@ -105,7 +105,7 @@ class FadeParticle(LifetimeParticle): def __init__( self, filename_or_texture: PathOrTexture, - change_xy: Vector, + change_xy: Velocity, lifetime: float, center_xy: Point = (0.0, 0.0), angle: float = 0, diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 3a30076aa6..b28821b7a4 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -96,6 +96,7 @@ "RGBOrA255", "RGBANormalized", "TiledObject", + "Velocity" ] @@ -104,6 +105,8 @@ Point3 = Tuple[AsFloat, AsFloat, AsFloat] IPoint = Tuple[int, int] +# We won't keep this forever. It's a temp stub for particles we'll replace. +Velocity = Tuple[AsFloat, AsFloat] PointList = Sequence[Point] # Speed / typing workaround: From f8e0a42b79d84c46e31d3ee3db36add1791ebf15 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 06:53:43 -0400 Subject: [PATCH 20/31] Add Point3 to arcade.types.__all__ --- arcade/types/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index b28821b7a4..152194622d 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -84,6 +84,7 @@ "PathOr", "PathOrTexture", "Point", + "Point3", "PointList", "EMPTY_POINT_LIST", "Rect", From ea53edfe66288b10c6e6bf3fa14a43aa5be86d43 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 06:55:53 -0400 Subject: [PATCH 21/31] Use consistent Optional type for LifeTimePartic.__init__'s filename_or_texture --- arcade/particles/particle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/particles/particle.py b/arcade/particles/particle.py index f55cc02f19..a5bfa94de4 100644 --- a/arcade/particles/particle.py +++ b/arcade/particles/particle.py @@ -74,7 +74,7 @@ class LifetimeParticle(Particle): def __init__( self, - filename_or_texture: PathOrTexture, + filename_or_texture: Optional[PathOrTexture], change_xy: Velocity, lifetime: float, center_xy: Point = (0.0, 0.0), From 058968a14f4dba0391830230511f339662b8b7de Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 06:58:39 -0400 Subject: [PATCH 22/31] Skip linting import order in arcade.types --- arcade/types/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 152194622d..8b72458309 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -21,6 +21,8 @@ """ from __future__ import annotations +# Don't lint import order since we have conditional compatibility shims +# flake8: noqa: E402 import sys from pathlib import Path from typing import ( From 92bde13bc44537e69d75e1cb01df36f865754f95 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 07:03:52 -0400 Subject: [PATCH 23/31] noqa linters wrongly marking a type checking import as unused --- arcade/types/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 8b72458309..442e4ad5df 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -131,7 +131,8 @@ # Specific utility resource aliases with type imports if TYPE_CHECKING: - from arcade.texture import Texture + # The linters are wrong: this is used, so we noqa it + from arcade.texture import Texture # flake8: noqa: F401 PathOrTexture = PathOr["Texture"] From 5db891772abe687ec3ecb727aa299935814ade83 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 07:06:03 -0400 Subject: [PATCH 24/31] Add omitted color tuple aliases to __all__ --- arcade/types/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 442e4ad5df..eeb3c80247 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -92,12 +92,13 @@ "Rect", "RectList", "RGB", + "RGBA", "RGBOrA", "RGBA255", "RGB255", "RGBNormalized", "RGBOrA255", - "RGBANormalized", + "RGBOrANormalized", "TiledObject", "Velocity" ] From cae09200e6ba05f16d816cc11c63bf5af8a0ba4d Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 07:08:36 -0400 Subject: [PATCH 25/31] Mark color masking constants with Final[int] --- arcade/types/color.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/arcade/types/color.py b/arcade/types/color.py index 9369535b88..9eb43494b5 100644 --- a/arcade/types/color.py +++ b/arcade/types/color.py @@ -22,7 +22,7 @@ import random from typing import Tuple, Iterable, Optional, Union, TypeVar -from typing_extensions import Self +from typing_extensions import Self, Final from arcade.utils import ByteRangeError, IntOutsideRangeError, NormalizedRangeError @@ -51,15 +51,15 @@ # Helpful color-related constants for bit masking -MAX_UINT24 = 0xFFFFFF -MAX_UINT32 = 0xFFFFFFFF -MASK_RGBA_R = 0xFF000000 -MASK_RGBA_G = 0x00FF0000 -MASK_RGBA_B = 0x0000FF00 -MASK_RGBA_A = 0x000000FF -MASK_RGB_R = 0xFF0000 -MASK_RGB_G = 0x00FF00 -MASK_RGB_B = 0x0000FF +MAX_UINT24: Final[int] = 0xFFFFFF +MAX_UINT32: Final[int] = 0xFFFFFFFF +MASK_RGBA_R: Final[int] = 0xFF000000 +MASK_RGBA_G: Final[int] = 0x00FF0000 +MASK_RGBA_B: Final[int] = 0x0000FF00 +MASK_RGBA_A: Final[int] = 0x000000FF +MASK_RGB_R: Final[int] = 0xFF0000 +MASK_RGB_G: Final[int] = 0x00FF00 +MASK_RGB_B: Final[int] = 0x0000FF # Color type aliases. From bfc30698a15476bd11d6ba34b3500e03df7521cf Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 07:10:38 -0400 Subject: [PATCH 26/31] Fix noqa prefix for conditional Texture import --- arcade/types/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index eeb3c80247..84fc2f4747 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -133,7 +133,7 @@ # Specific utility resource aliases with type imports if TYPE_CHECKING: # The linters are wrong: this is used, so we noqa it - from arcade.texture import Texture # flake8: noqa: F401 + from arcade.texture import Texture # noqa: F401 PathOrTexture = PathOr["Texture"] From 272b3c4efb22427a463a07044dcd477133c2ace3 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 07:12:22 -0400 Subject: [PATCH 27/31] Let someone else alphabetize the arcade.types.__all__ declaration; I'm just getting it to build. --- arcade/types/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 84fc2f4747..b657e0cc7f 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -94,10 +94,11 @@ "RGB", "RGBA", "RGBOrA", - "RGBA255", "RGB255", - "RGBNormalized", + "RGBA255", "RGBOrA255", + "RGBNormalized", + "RGBANormalized", "RGBOrANormalized", "TiledObject", "Velocity" From 71f2550878cc6d37ce70ec045944557c221b12f6 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 07:25:19 -0400 Subject: [PATCH 28/31] Document Size2D[int] and Size2D[float] --- arcade/types/__init__.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index b657e0cc7f..3a59b0b5dd 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -105,11 +105,33 @@ ] +_T = TypeVar('_T') + +#: ``Size2D`` helps mark int or float sizes. Use it like a +#: :py:class:`typing.Generic`'s bracket notation as follows: +#: +#: .. code-block:: python +#: +#: def example_Function(size: Size2D[int], color: RGBA255) -> Texture: +#: """An example of how to use Size2D. +#: +#: Look at signature above, not the missing function body. The +#: ``size`` argument is how you can mark texture sizes, while +#: you can use ``Size2D[float]`` to denote float regions. +#: +#: :param size: A made-up hypothetical argument. +#: :param color: Hypothetical texture-related argument. +#: """ +#: ... # No function definition +#: +Size2D = Tuple[_T, _T] + # Point = Union[Tuple[AsFloat, AsFloat], List[AsFloat]] Point = Tuple[AsFloat, AsFloat] Point3 = Tuple[AsFloat, AsFloat, AsFloat] IPoint = Tuple[int, int] + # We won't keep this forever. It's a temp stub for particles we'll replace. Velocity = Tuple[AsFloat, AsFloat] From 3a54e4df2009f3bd557f2347c13a11c8b5a9089e Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 07:38:21 -0400 Subject: [PATCH 29/31] Add types to the API doc build --- util/update_quick_index.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/util/update_quick_index.py b/util/update_quick_index.py index 877ca0b3f3..b9a65d114a 100644 --- a/util/update_quick_index.py +++ b/util/update_quick_index.py @@ -55,7 +55,8 @@ 'texture_atlas/base.py': ['Texture Atlas', 'texture_atlas.rst'], 'texture_atlas/atlas_2d.py': ['Texture Atlas', 'texture_atlas.rst'], 'math.py': ['Math', 'math.rst'], - 'types.py': ['Types', 'types.rst'], + 'types/__init__.py': ['Types', 'types.rst'], + 'types/color.py': ['Types', 'types.rst'], 'easing.py': ['Easing', 'easing.rst'], 'earclip.py': ['Earclip', 'earclip.rst'], 'tilemap/__init__.py': ['Loading TMX (Tiled Map Editor) Maps', 'tiled.rst'], @@ -211,7 +212,7 @@ def process_directory(directory: Path, quick_index_file): "transforms.py": "arcade.texture.transforms", "isometric.py": "arcade.isometric", "particles": "arcade.particles", - "types.py": "arcade.types", + "types": "arcade.types", "utils.py": "arcade.utils", "easing.py": "arcade.easing", "math.py": "arcade.math", @@ -330,6 +331,7 @@ def main(): text_file.write(table_header_arcade) process_directory(ROOT / "arcade", text_file) + process_directory(ROOT / "arcade/types", text_file) process_directory(ROOT / "arcade/sprite_list", text_file) process_directory(ROOT / "arcade/geometry", text_file) process_directory(ROOT / "arcade/sprite", text_file) From 810288740df6dd489dfa3728aade2807407b53bb Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 07:45:18 -0400 Subject: [PATCH 30/31] Add Size2D to types.__all__ --- arcade/types/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 3a59b0b5dd..b8753ec064 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -100,6 +100,7 @@ "RGBNormalized", "RGBANormalized", "RGBOrANormalized", + "Size2D", "TiledObject", "Velocity" ] From 44e1a08e3b86e6606b6e21245c93687847559961 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 18 Apr 2024 07:45:56 -0400 Subject: [PATCH 31/31] Replace IPoint with Size2D in arcade.texture.tools --- arcade/texture/tools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arcade/texture/tools.py b/arcade/texture/tools.py index 572e6b7302..b8891d69aa 100644 --- a/arcade/texture/tools.py +++ b/arcade/texture/tools.py @@ -4,7 +4,7 @@ import arcade import arcade.cache from .texture import ImageData, Texture -from arcade.types import IPoint +from arcade.types import Size2D from arcade import cache _DEFAULT_TEXTURE = None @@ -20,7 +20,7 @@ def cleanup_texture_cache(): arcade.cache.image_data_cache.clear() -def get_default_texture(size: IPoint = _DEFAULT_IMAGE_SIZE) -> Texture: +def get_default_texture(size: Size2D[int] = _DEFAULT_IMAGE_SIZE) -> Texture: """ Creates and returns a default texture and caches it internally for future use. @@ -38,7 +38,7 @@ def get_default_texture(size: IPoint = _DEFAULT_IMAGE_SIZE) -> Texture: return _DEFAULT_TEXTURE -def get_default_image(size: IPoint = _DEFAULT_IMAGE_SIZE) -> ImageData: +def get_default_image(size: Size2D[int] = _DEFAULT_IMAGE_SIZE) -> ImageData: """ Generates and returns a default image and caches it internally for future use.