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/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/math.py b/arcade/math.py index 636cafb6e5..8e9fa1b911 100644 --- a/arcade/math.py +++ b/arcade/math.py @@ -2,9 +2,9 @@ import math import random -from typing import Tuple, List, Union -from pyglet.math import Vec2, Vec3 -from arcade.types import Point, Vector +from typing import Sequence, Tuple, Union +from arcade.types import AsFloat, Point + _PRECISION = 2 @@ -63,13 +63,13 @@ def clamp(a, low: float, high: float) -> float: return high if a > high else max(a, low) -def lerp(v1: float, v2: float, u: float) -> float: - """linearly interpolate between two values""" - return v1 + ((v2 - v1) * u) +V_2D = Union[Tuple[AsFloat, AsFloat], Sequence[AsFloat]] +V_3D = Union[Tuple[AsFloat, AsFloat, AsFloat], Sequence[AsFloat]] -V_2D = Union[Vec2, Tuple[float, float], List[float]] -V_3D = Union[Vec3, Tuple[float, float, float], List[float]] +def lerp(v1: AsFloat, v2: AsFloat, u: float) -> float: + """linearly interpolate between two values""" + return v1 + ((v2 - v1) * u) def lerp_2d(v1: V_2D, v2: V_2D, u: float) -> Tuple[float, float]: @@ -202,7 +202,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. @@ -220,7 +220,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/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 9cc3bcba7d..a5bfa94de4 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, Velocity 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, @@ -54,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,8 +74,8 @@ class LifetimeParticle(Particle): def __init__( self, - filename_or_texture: PathOrTexture, - change_xy: Vector, + filename_or_texture: Optional[PathOrTexture], + change_xy: Velocity, lifetime: float, center_xy: Point = (0.0, 0.0), angle: float = 0, @@ -106,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/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/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. diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py new file mode 100644 index 0000000000..b8753ec064 --- /dev/null +++ b/arcade/types/__init__.py @@ -0,0 +1,169 @@ +"""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 + +# Don't lint import order since we have conditional compatibility shims +# flake8: noqa: E402 +import sys +from pathlib import Path +from typing import ( + List, + NamedTuple, + Optional, + Sequence, + 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] + + +#: 1. Makes pyright happier while also telling readers +#: 2. Tells readers we're converting any ints to floats +AsFloat = Union[float, int] + + +# Generic color aliases +from arcade.types.color import RGB +from arcade.types.color import RGBA +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 + +# The Color helper type +from arcade.types.color import Color + + +__all__ = [ + "AsFloat", + "BufferProtocol", + "Color", + "IPoint", + "PathOr", + "PathOrTexture", + "Point", + "Point3", + "PointList", + "EMPTY_POINT_LIST", + "Rect", + "RectList", + "RGB", + "RGBA", + "RGBOrA", + "RGB255", + "RGBA255", + "RGBOrA255", + "RGBNormalized", + "RGBANormalized", + "RGBOrANormalized", + "Size2D", + "TiledObject", + "Velocity" +] + + +_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] + +PointList = Sequence[Point] +# Speed / typing workaround: +# 1. Eliminate extra allocations +# 2. Allows type annotation to be cleaner, primarily for HitBox & subclasses +EMPTY_POINT_LIST: PointList = tuple() + + +Rect = Union[Tuple[int, int, int, int], List[int]] # x, y, width, height +RectList = Union[Tuple[Rect, ...], List[Rect]] +FloatRect = Union[Tuple[AsFloat, AsFloat, AsFloat, AsFloat], List[AsFloat]] # x, y, width, height + + +# 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: + # The linters are wrong: this is used, so we noqa it + from arcade.texture import Texture # noqa: F401 + +PathOrTexture = PathOr["Texture"] + + +class TiledObject(NamedTuple): + shape: Union[Point, PointList, Rect] + properties: Optional[Properties] = None + name: Optional[str] = None + type: Optional[str] = None diff --git a/arcade/types.py b/arcade/types/color.py similarity index 82% rename from arcade/types.py rename to arcade/types/color.py index b8e328fa47..9eb43494b5 100644 --- a/arcade/types.py +++ b/arcade/types/color.py @@ -1,90 +1,82 @@ -""" -Module specifying data custom types used for type hinting. -""" -from __future__ import annotations +"""Color-related types, aliases, and constants. -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, - Sequence, - Tuple, - Union, - TYPE_CHECKING, - TypeVar -) -from typing_extensions import Self +This module does not contain pre-defined color values. For pre-made +named color values, please see the following: -from pytiled_parser import Properties +.. list-table:: + :header-rows: 1 -from arcade.utils import ( - IntOutsideRangeError, - ByteRangeError, - NormalizedRangeError -) + * - Module + - Contents -if TYPE_CHECKING: - from arcade.texture import Texture + * - :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. -#: 1. Makes pyright happier while also telling readers -#: 2. Tells readers we're converting any ints to floats -AsFloat = Union[float, int] +""" +from __future__ import annotations -MAX_UINT24 = 0xFFFFFF -MAX_UINT32 = 0xFFFFFFFF +import random +from typing import Tuple, Iterable, Optional, Union, TypeVar + +from typing_extensions import Self, Final + +from arcade.utils import ByteRangeError, IntOutsideRangeError, NormalizedRangeError + + +__all__ = ( + 'Color', + 'RGB', + 'RGBA', + 'RGB255', + 'RGBA255', + 'RGBNormalized', + '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', +) -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 +# Helpful color-related constants for bit masking +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. ChannelType = TypeVar('ChannelType') +# Generic color aliases RGB = Tuple[ChannelType, ChannelType, ChannelType] RGBA = Tuple[ChannelType, ChannelType, ChannelType, ChannelType] RGBOrA = Union[RGB[ChannelType], RGBA[ChannelType]] -RGBOrA255 = RGBOrA[int] -RGBOrANormalized = RGBOrA[float] - +# Specific color aliases +RGB255 = RGB[int] RGBA255 = RGBA[int] +RGBNormalized = RGB[float] RGBANormalized = RGBA[float] - -RGBA255OrNormalized = Union[RGBA255, RGBANormalized] - - -__all__ = [ - "BufferProtocol", - "Color", - "ColorLike", - "IPoint", - "PathOrTexture", - "Point", - "PointList", - "EMPTY_POINT_LIST", - "NamedPoint", - "Rect", - "RectList", - "RGB", - "RGBA255", - "RGBANormalized", - "RGBA255OrNormalized", - "TiledObject", - "Vector" -] +RGBOrA255 = RGBOrA[int] +RGBOrANormalized = RGBOrA[float] class Color(RGBA255): @@ -487,45 +479,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) - - -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 -NamedPoint = namedtuple("NamedPoint", ["x", "y"]) - - -PointList = Sequence[Point] -# Speed / typing workaround: -# 1. Eliminate extra allocations -# 2. Allows type annotation to be cleaner, primarily for HitBox & subclasses -EMPTY_POINT_LIST: PointList = tuple() - - -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 - -PathOrTexture = Optional[Union[str, Path, "Texture"]] - - -class TiledObject(NamedTuple): - shape: Union[Point, PointList, Rect] - properties: Optional[Properties] = None - name: Optional[str] = None - 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] 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)