From 79201fce098e9cfd031b0023aa0f59e9e0205df8 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Sun, 26 May 2024 01:00:09 -0400 Subject: [PATCH 01/52] add .rect to BasicSprite --- arcade/sprite/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 26a45e270f..b2acd50bf3 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -7,6 +7,7 @@ from arcade.color import BLACK, WHITE from arcade.hitbox import HitBox from arcade.texture import Texture +from arcade.types.rect import Rect, LRBT from arcade.utils import copy_dunders_unimplemented if TYPE_CHECKING: @@ -296,6 +297,10 @@ def top(self, amount: float): diff = highest - amount self.center_y -= diff + @property + def rect(self) -> Rect: + return LRBT(self.left, self.right, self.bottom, self.top) + @property def visible(self) -> bool: """Get or set the visibility of this sprite. From 97444a8e8e0a4dd0072de756a59c1f193dc838d2 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Sun, 26 May 2024 01:01:52 -0400 Subject: [PATCH 02/52] fix minor linting issues in BasicSprite --- arcade/sprite/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index b2acd50bf3..f8a7c01733 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -17,7 +17,7 @@ SpriteType = TypeVar("SpriteType", bound="BasicSprite") -@copy_dunders_unimplemented # See https://github.com/pythonarcade/arcade/issues/2074 +@copy_dunders_unimplemented # See https://github.com/pythonarcade/arcade/issues/2074 class BasicSprite: """ The absolute minimum needed for a sprite. @@ -365,7 +365,7 @@ def rgb(self, color: RGBOrA255): if len(_a) > 1: # Alpha's only used to validate here raise ValueError() - except ValueError as _: # It's always a length issue + except ValueError: # It's always a length issue raise ValueError(( f"{self.__class__.__name__},rgb takes 3 or 4 channel" f" colors, but got {len(color)} channels")) From 91b124bcbbd3350988206a5a2a49218eb429ea69 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Sun, 26 May 2024 01:07:28 -0400 Subject: [PATCH 03/52] texture.draw_rect --- arcade/texture/texture.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/arcade/texture/texture.py b/arcade/texture/texture.py index cf2d50c7ec..bec763f609 100644 --- a/arcade/texture/texture.py +++ b/arcade/texture/texture.py @@ -27,6 +27,7 @@ ) from arcade.types import RGBA255, PointList +from arcade.types.rect import Rect if TYPE_CHECKING: from arcade import TextureAtlas @@ -860,6 +861,19 @@ def draw_scaled( spritelist.draw() spritelist.remove(sprite) + def draw_rect(self, rect: Rect, alpha: int = 255): + """ + Draw the texture. + + .. warning:: This is a very slow method of drawing a texture, + and should be used sparingly. The method simply + creates a sprite internally and draws it. + + :param rect: A Rect to draw this texture to. + :param alpha: The transparency of the texture `(0-255)`. + """ + self.draw_sized(rect.x, rect.y, rect.width, rect.height, alpha=alpha) + # ------------------------------------------------------------ # Comparison and hash functions so textures can work with sets # A texture's uniqueness is simply based on the name From 82cd7a3047d0f81e5adefcfa108f591db0501c8f Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Sun, 26 May 2024 01:25:18 -0400 Subject: [PATCH 04/52] rect.distance_from_bounds --- arcade/types/rect.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/arcade/types/rect.py b/arcade/types/rect.py index b860c7099d..bca9a5d9c6 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -343,14 +343,17 @@ def point_in_rect(self, point: Vec2) -> bool: """Returns True if the given point is inside this rectangle.""" return (self.left < point.x < self.right) and (self.bottom < point.y < self.top) - def point_on_bounds(self, point: Vec2, tolerance: float) -> bool: - """Returns True if the given point is on the bounds of this rectangle within some tolerance.""" + def distance_from_bounds(self, point: Vec2) -> float: + """Returns the point's distance from the boundary of this rectangle.""" diff = Vec2(point.x - self.x, point.y - self.y) dx = abs(diff.x) - self.width / 2.0 dy = abs(diff.y) - self.height / 2.0 d = (max(dx, 0.0)**2 + max(dy, 0.0)**2)**0.5 + min(max(dx, dy), 0.0) + return d - return abs(d) < tolerance + def point_on_bounds(self, point: Vec2, tolerance: float) -> bool: + """Returns True if the given point is on the bounds of this rectangle within some tolerance.""" + return abs(self.distance_from_bounds(point)) < tolerance def to_points(self) -> tuple[Vec2, Vec2, Vec2, Vec2]: """Returns a tuple of the four corners of this Rect.""" From 514a5aa542c9e740abd81140e26fea6c7b53c2e3 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Sun, 26 May 2024 01:37:56 -0400 Subject: [PATCH 05/52] add Rect and Vec to top level --- arcade/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/arcade/__init__.py b/arcade/__init__.py index e27a38f11d..1b22edd218 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -218,6 +218,8 @@ def configure_logging(level: Optional[int] = None): from .camera import Camera2D +from .types.rect import Rect, LRBT, LBWH, XYWH + # Module imports from arcade import color as color from arcade import csscolor as csscolor @@ -231,6 +233,9 @@ def configure_logging(level: Optional[int] = None): from arcade import experimental as experimental from arcade.types import rect +# For ease of access for beginners +from pyglet.math import Vec2, Vec3, Vec4 + from .text import ( draw_text, load_font, @@ -261,6 +266,10 @@ def configure_logging(level: Optional[int] = None): 'PymunkException', 'PymunkPhysicsEngine', 'PymunkPhysicsObject', + 'Rect', + 'LBWH', + 'LRBT', + 'XYWH', 'Section', 'SectionManager', 'Scene', @@ -280,6 +289,9 @@ def configure_logging(level: Optional[int] = None): 'TextureAtlas', 'TileMap', 'VERSION', + 'Vec2', + 'Vec3', + 'Vec4', 'View', 'Window', 'astar_calculate_path', From 40cc4537712ae420ee545c1627c643a1f4deeb1f Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Tue, 28 May 2024 19:45:31 -0400 Subject: [PATCH 06/52] docstring fixes --- arcade/texture/texture.py | 4 ++-- arcade/types/rect.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/arcade/texture/texture.py b/arcade/texture/texture.py index bec763f609..58ecf5d895 100644 --- a/arcade/texture/texture.py +++ b/arcade/texture/texture.py @@ -843,7 +843,7 @@ def draw_scaled( :param center_y: Y location of where to draw the texture. :param scale: Scale to draw rectangle. Defaults to 1. :param angle: Angle to rotate the texture by. - :param alpha: The transparency of the texture `(0-255)`. + :param alpha: The transparency of the texture ``(0-255)``. """ from arcade import Sprite @@ -870,7 +870,7 @@ def draw_rect(self, rect: Rect, alpha: int = 255): creates a sprite internally and draws it. :param rect: A Rect to draw this texture to. - :param alpha: The transparency of the texture `(0-255)`. + :param alpha: The transparency of the texture ``(0-255)``. """ self.draw_sized(rect.x, rect.y, rect.width, rect.height, alpha=alpha) diff --git a/arcade/types/rect.py b/arcade/types/rect.py index bca9a5d9c6..e6c8dc78c3 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -25,7 +25,7 @@ class RectKwargs(TypedDict): class Rect(NamedTuple): - """Rects define a rectangle, with several convenience properties and functions. + """``Rect``s define a rectangle, with several convenience properties and functions. This object is immutable by design. It provides no setters, and is a NamedTuple subclass. @@ -35,10 +35,9 @@ class Rect(NamedTuple): Rectangles cannot rotate by design, since this complicates their implmentation a lot. You probably don't want to create one of these directly, and should instead use a helper method, like - :py:func:`~arcade.types.rect.LBWH`, :py:func:`~arcade.types.rect.LRBT`, - :py:func:`~arcade.types.rect.XYWH`, or :py:func:`~arcade.types.rect.Viewport`. + :py:func:`LBWH`, :py:func:`LRBT`, :py:func:`XYWH`, or :py:func:`Viewport`. - You can also use :py:func:`~arcade.types.rect.Rect.from_kwargs` to create a Rect from keyword arguments. + You can also use :py:meth:`.from_kwargs` to create a Rect from keyword arguments. """ #: The X position of the rectangle's left edge. @@ -309,6 +308,7 @@ def union(self, other: Rect) -> Rect: return LRBT(left, right, bottom, top) def __or__(self, other: Rect) -> Rect: + """Returns the result of :py:meth:`union` with ``other``.""" return self.union(other) def intersection(self, other: Rect) -> Rect | None: @@ -326,6 +326,7 @@ def intersection(self, other: Rect) -> Rect | None: return LRBT(left, right, bottom, top) def __and__(self, other: Rect) -> Rect | None: + """Returns the result of :py:meth:`intersection` with ``other``.""" return self.intersection(other) def overlaps(self, other: Rect) -> bool: @@ -398,9 +399,10 @@ def from_kwargs(cls, **kwargs: AsFloat) -> Rect: """Creates a new Rect from keyword arguments. Throws ValueError if not enough are provided. Expected forms are: - * LRBT (providing `left`, `right`, `bottom`, and `top`) - * LBWH (providing `left`, `bottom`, `width`, and `height`) - * XYWH (providing `x`, `y`, `width`, and `height`) + + * LRBT (providing ``left``, ``right``, ``bottom``, and ``top``) + * LBWH (providing ``left``, ``bottom``, ``width``, and ``height``) + * XYWH (providing ``x``, ``y``, ``width``, and ``height``) """ # Perform iteration only once and store it as a set literal specified: set[str] = {k for k, v in kwargs.items() if v is not None} From 41322bc27f6419fe355f345a1244c77720616e84 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Tue, 28 May 2024 18:47:34 -0500 Subject: [PATCH 07/52] Remove formatted style at opening of docstring --- arcade/types/rect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/types/rect.py b/arcade/types/rect.py index e6c8dc78c3..c0e5377b5c 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -25,7 +25,7 @@ class RectKwargs(TypedDict): class Rect(NamedTuple): - """``Rect``s define a rectangle, with several convenience properties and functions. + """A rectangle, with several convenience properties and functions. This object is immutable by design. It provides no setters, and is a NamedTuple subclass. From 4db435c3175d90647fb198b8d22ef4a1a36cab05 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Tue, 28 May 2024 20:20:46 -0400 Subject: [PATCH 08/52] __contains__ and doc Kwargs --- arcade/types/rect.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/arcade/types/rect.py b/arcade/types/rect.py index c0e5377b5c..0d1766d55c 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -14,6 +14,11 @@ class RectKwargs(TypedDict): + """A dictionary of the eight canon properties of a rectangle. + + ``left``, ``right``, ``bottom``, ``top``, + ``width``, ``height``, ``x``, and ``y`` are all :py:`float`s. + """ left: float right: float bottom: float @@ -35,7 +40,7 @@ class Rect(NamedTuple): Rectangles cannot rotate by design, since this complicates their implmentation a lot. You probably don't want to create one of these directly, and should instead use a helper method, like - :py:func:`LBWH`, :py:func:`LRBT`, :py:func:`XYWH`, or :py:func:`Viewport`. + :py:func:`.LBWH`, :py:func:`.LRBT`, :py:func:`.XYWH`, or :py:func:`.Viewport`. You can also use :py:meth:`.from_kwargs` to create a Rect from keyword arguments. @@ -344,6 +349,10 @@ def point_in_rect(self, point: Vec2) -> bool: """Returns True if the given point is inside this rectangle.""" return (self.left < point.x < self.right) and (self.bottom < point.y < self.top) + def __contains__(self, point: Vec2) -> bool: + """Returns the result of :py:meth:`point_in_rect` with a provided point.""" + return self.point_in_rect(point) + def distance_from_bounds(self, point: Vec2) -> float: """Returns the point's distance from the boundary of this rectangle.""" diff = Vec2(point.x - self.x, point.y - self.y) From 50292209b4bf30b9a0d55e6f8adff0dbbdd4cced Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Tue, 28 May 2024 20:40:44 -0400 Subject: [PATCH 09/52] doc weee --- arcade/types/rect.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/arcade/types/rect.py b/arcade/types/rect.py index 0d1766d55c..ab6b044cfd 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -17,7 +17,7 @@ class RectKwargs(TypedDict): """A dictionary of the eight canon properties of a rectangle. ``left``, ``right``, ``bottom``, ``top``, - ``width``, ``height``, ``x``, and ``y`` are all :py:`float`s. + ``width``, ``height``, ``x``, and ``y`` are all :py:class:`float`s. """ left: float right: float @@ -454,7 +454,7 @@ def __str__(self) -> str: # Shorthand creation helpers def LRBT(left: AsFloat, right: AsFloat, bottom: AsFloat, top: AsFloat) -> Rect: - """Creates a new Rect from left, right, bottom, and top parameters.""" + """Creates a new :py:class:`.Rect` from left, right, bottom, and top parameters.""" width = right - left height = top - bottom x = left + (width / 2) @@ -463,7 +463,7 @@ def LRBT(left: AsFloat, right: AsFloat, bottom: AsFloat, top: AsFloat) -> Rect: def LBWH(left: AsFloat, bottom: AsFloat, width: AsFloat, height: AsFloat) -> Rect: - """Creates a new Rect from left, bottom, width, and height parameters.""" + """Creates a new :py:class:`.Rect` from left, bottom, width, and height parameters.""" right = left + width top = bottom + height x = left + (width / 2) @@ -472,7 +472,7 @@ def LBWH(left: AsFloat, bottom: AsFloat, width: AsFloat, height: AsFloat) -> Rec def XYWH(x: AsFloat, y: AsFloat, width: AsFloat, height: AsFloat, anchor: Vec2 = AnchorPoint.CENTER) -> Rect: - """Creates a new Rect from x, y, width, and height parameters, anchored at a relative point (default center).""" + """Creates a new :py:class:`.Rect` from x, y, width, and height parameters, anchored at a relative point (default center).""" left = x - anchor.x * width right = left + width bottom = y - anchor.y * height @@ -484,7 +484,7 @@ def XYWH(x: AsFloat, y: AsFloat, width: AsFloat, height: AsFloat, anchor: Vec2 = def XYRR(x: AsFloat, y: AsFloat, half_width: AsFloat, half_height: AsFloat) -> Rect: """ - Creates a new Rect from center x, center y, half width, and half height parameters. + Creates a new :py:class:`.Rect` from center x, center y, half width, and half height parameters. This is mainly used by OpenGL. """ left = x - half_width @@ -495,7 +495,7 @@ def XYRR(x: AsFloat, y: AsFloat, half_width: AsFloat, half_height: AsFloat) -> R def Viewport(left: int, bottom: int, width: int, height: int) -> Rect: - """Creates a new Rect from left, bottom, width, and height parameters, restricted to integers.""" + """Creates a new :py:class:`.Rect` from left, bottom, width, and height parameters, restricted to integers.""" right = left + width top = bottom + height x = left + int(width / 2) From 6169edf361e680732e9da1ffbeecd49777d2d370 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Tue, 28 May 2024 22:57:10 -0400 Subject: [PATCH 10/52] start changelog, add Point2 and Point3 --- PR.md | 10 ++++++++++ arcade/types/__init__.py | 4 ++-- arcade/types/vector_like.py | 11 +++++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 PR.md diff --git a/PR.md b/PR.md new file mode 100644 index 0000000000..ee5b4d3ef6 --- /dev/null +++ b/PR.md @@ -0,0 +1,10 @@ +# The Rect, Part II + +- Added type aliases `Point2` and `Point3` +- `Rect` + - Added `Rect.distance_from_bounds()` + - Added `point in rect` support for `Rect` + - Improved docstrings +- Added `Rect` and it's constructors, `Vec2`, and `Vec3` to top-level module +- Added `Texture.draw_rect()` +- Added `BasicSprite.rect` diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 309ecadc5a..40806959ce 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -77,7 +77,7 @@ from arcade.types.color import Color # We'll be moving our Vec-like items into this (Points, Sizes, etc) -from arcade.types.vector_like import AnchorPoint +from arcade.types.vector_like import AnchorPoint, Point2, Point3 # Rectangles from arcade.types.rect import ViewportParams @@ -98,7 +98,7 @@ "IPoint", "PathOr", "PathOrTexture", - "Point", + "Point2", "Point3", "PointList", "EMPTY_POINT_LIST", diff --git a/arcade/types/vector_like.py b/arcade/types/vector_like.py index b148383c1d..8dc78786dc 100644 --- a/arcade/types/vector_like.py +++ b/arcade/types/vector_like.py @@ -7,9 +7,14 @@ """ from __future__ import annotations +from typing import Union +from pyglet.math import Vec2, Vec3 -from pyglet.math import Vec2 +from arcade.types.numbers import AsFloat + +Point2 = Union[tuple[AsFloat, AsFloat], Vec2] +Point3 = Union[tuple[AsFloat, AsFloat, AsFloat], Vec3] class AnchorPoint: @@ -26,5 +31,7 @@ class AnchorPoint: __all__ = [ - 'AnchorPoint' + 'AnchorPoint', + 'Point2', + 'Point3' ] From 130910b32ff2136d5ec4ca3ec4b062405345aca1 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Tue, 28 May 2024 22:58:45 -0400 Subject: [PATCH 11/52] add Point back --- arcade/types/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 40806959ce..330b273cdc 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -98,6 +98,7 @@ "IPoint", "PathOr", "PathOrTexture", + "Point", "Point2", "Point3", "PointList", @@ -148,11 +149,8 @@ #: ... # 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] +Point = Union[Point2, Point3] # We won't keep this forever. It's a temp stub for particles we'll replace. From bf05ca51eb891685cc2e9a6c23070d41ff099c74 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Tue, 28 May 2024 23:14:34 -0400 Subject: [PATCH 12/52] Remove IntRect, FloatRect, RectList --- PR.md | 1 + arcade/gui/surface.py | 9 +++++---- arcade/sprite_list/collision.py | 12 ++++-------- arcade/sprite_list/spatial_hash.py | 7 ++++--- arcade/texture/loading.py | 4 ++-- arcade/texture/spritesheet.py | 20 +------------------- arcade/tilemap/tilemap.py | 4 ++-- arcade/types/__init__.py | 10 +--------- 8 files changed, 20 insertions(+), 47 deletions(-) diff --git a/PR.md b/PR.md index ee5b4d3ef6..8c5d61c54d 100644 --- a/PR.md +++ b/PR.md @@ -8,3 +8,4 @@ - Added `Rect` and it's constructors, `Vec2`, and `Vec3` to top-level module - Added `Texture.draw_rect()` - Added `BasicSprite.rect` +- Remove `IntRect`, `FloatRect`, `RectList` diff --git a/arcade/gui/surface.py b/arcade/gui/surface.py index 2c42e26ded..a5d41ccb6a 100644 --- a/arcade/gui/surface.py +++ b/arcade/gui/surface.py @@ -10,7 +10,8 @@ from arcade.camera import OrthographicProjector, OrthographicProjectionData, CameraData from arcade.gl import Framebuffer from arcade.gui.nine_patch import NinePatchTexture -from arcade.types import RGBA255, FloatRect, Point +from arcade.types import RGBA255, Point +from arcade.types.rect import Rect class Surface: @@ -174,7 +175,7 @@ def limit(self, x, y, width, height): def draw( self, - area: Optional[FloatRect] = None, + area: Optional[Rect] = None, ) -> None: """ Draws the contents of the surface. @@ -182,7 +183,7 @@ def draw( The surface will be rendered at the configured ``position`` and limited by the given ``area``. The area can be out of bounds. - :param area: Limit the area in the surface we're drawing (x, y, w, h) + :param area: Limit the area in the surface we're drawing (l, b, w, h) """ # Set blend function blend_func = self.ctx.blend_func @@ -191,7 +192,7 @@ def draw( self.texture.use(0) self._program["pos"] = self._pos self._program["size"] = self._size - self._program["area"] = area or (0, 0, *self._size) + self._program["area"] = area.lbwh or (0, 0, *self._size) self._geometry.render(self._program, vertices=1) # Restore blend function diff --git a/arcade/sprite_list/collision.py b/arcade/sprite_list/collision.py index 7601b8a4d2..ed73ed29b9 100644 --- a/arcade/sprite_list/collision.py +++ b/arcade/sprite_list/collision.py @@ -17,7 +17,8 @@ ) from arcade.math import get_distance from arcade.sprite import BasicSprite, SpriteType -from arcade.types import Point, IntRect +from arcade.types import Point +from arcade.types.rect import Rect from .sprite_list import SpriteList @@ -324,7 +325,7 @@ def get_sprites_at_exact_point(point: Point, sprite_list: SpriteList[SpriteType] return [s for s in sprites_to_check if s.position == point] -def get_sprites_in_rect(rect: IntRect, sprite_list: SpriteList[SpriteType]) -> List[SpriteType]: +def get_sprites_in_rect(rect: Rect, sprite_list: SpriteList[SpriteType]) -> List[SpriteType]: """ Get a list of sprites in a particular rectangle. This function sees if any sprite overlaps the specified rectangle. If a sprite has a different center_x/center_y but touches the rectangle, @@ -343,12 +344,7 @@ def get_sprites_in_rect(rect: IntRect, sprite_list: SpriteList[SpriteType]) -> L f"Parameter 2 is a {type(sprite_list)} instead of expected SpriteList." ) - rect_points = ( - (rect[0], rect[3]), - (rect[1], rect[3]), - (rect[1], rect[2]), - (rect[0], rect[2]), - ) + rect_points = rect.to_points() sprites_to_check: Iterable[SpriteType] if sprite_list.spatial_hash is not None: diff --git a/arcade/sprite_list/spatial_hash.py b/arcade/sprite_list/spatial_hash.py index e568b12cce..1804b3df4b 100644 --- a/arcade/sprite_list/spatial_hash.py +++ b/arcade/sprite_list/spatial_hash.py @@ -8,8 +8,9 @@ Generic, ) from arcade.sprite.base import BasicSprite -from arcade.types import Point, IPoint, IntRect +from arcade.types import Point, IPoint from arcade.sprite import SpriteType +from arcade.types.rect import Rect class SpatialHash(Generic[SpriteType]): @@ -129,14 +130,14 @@ def get_sprites_near_point(self, point: Point) -> Set[SpriteType]: # Return a copy of the set. return set(self.contents.setdefault(hash_point, set())) - def get_sprites_near_rect(self, rect: IntRect) -> Set[SpriteType]: + def get_sprites_near_rect(self, rect: Rect) -> Set[SpriteType]: """ Return sprites in the same buckets as the given rectangle. :param rect: The rectangle to check (left, right, bottom, top) :return: A set of sprites in the rectangle """ - left, right, bottom, top = rect + left, right, bottom, top = rect.lrbt min_point = trunc(left), trunc(bottom) max_point = trunc(right), trunc(top) diff --git a/arcade/texture/loading.py b/arcade/texture/loading.py index d8e8b51ddd..9c4c69fe3d 100644 --- a/arcade/texture/loading.py +++ b/arcade/texture/loading.py @@ -8,11 +8,11 @@ import PIL.ImageOps import PIL.ImageDraw -from arcade.types import RectList from arcade.resources import resolve from arcade.hitbox import HitBoxAlgorithm from arcade import cache as _cache from arcade import hitbox +from arcade.types.numbers import AsFloat from .texture import Texture, ImageData LOG = logging.getLogger(__name__) @@ -178,7 +178,7 @@ def load_texture_pair( def load_textures( file_name: Union[str, Path], - image_location_list: RectList, + image_location_list: List[Tuple[AsFloat, AsFloat, AsFloat, AsFloat]], mirrored: bool = False, flipped: bool = False, hit_box_algorithm: Optional[HitBoxAlgorithm] = None, diff --git a/arcade/texture/spritesheet.py b/arcade/texture/spritesheet.py index fda5a32e0e..552292a0e5 100644 --- a/arcade/texture/spritesheet.py +++ b/arcade/texture/spritesheet.py @@ -3,7 +3,6 @@ from typing import Union, Tuple, Optional, List, TYPE_CHECKING # from arcade import Texture -from arcade.types import IntRect from .texture import Texture if TYPE_CHECKING: @@ -94,29 +93,12 @@ def flip_top_bottom(self): self._image = self._image.transpose(Image.FLIP_TOP_BOTTOM) self._flip_flags = (self._flip_flags[0], not self._flip_flags[1]) - def crop(self, area: IntRect): - """ - Crop a texture from the sprite sheet. - - :param area: Area to crop ``(x, y, width, height)`` - """ - pass - - def crop_sections(self, sections: List[IntRect]): - """ - Crop multiple textures from the sprite sheet by specifying a list of - areas to crop. - - :param sections: List of areas to crop ``[(x, y, width, height), ...]`` - """ - pass - def crop_grid( self, size: Tuple[int, int], columns: int, count: int, - margin: IntRect = (0, 0, 0, 0), + margin: Tuple[int, int, int, int] = (0, 0, 0, 0), hit_box_algorithm: Optional["HitBoxAlgorithm"] = None, ) -> List[Texture]: """ diff --git a/arcade/tilemap/tilemap.py b/arcade/tilemap/tilemap.py index cd917fe2b7..43d8364773 100644 --- a/arcade/tilemap/tilemap.py +++ b/arcade/tilemap/tilemap.py @@ -38,7 +38,7 @@ from arcade.math import rotate_point from arcade.resources import resolve -from arcade.types import Point, IntRect, TiledObject +from arcade.types import Point, TiledObject _FLIPPED_HORIZONTALLY_FLAG = 0x80000000 _FLIPPED_VERTICALLY_FLAG = 0x40000000 @@ -831,7 +831,7 @@ def _process_object_layer( sprite_list: Optional[SpriteList] = None objects_list: Optional[List[TiledObject]] = [] - shape: Union[List[Point], IntRect, Point, None] = None + shape: Union[List[Point], Tuple[int, int, int, int], Point, None] = None for cur_object in layer.tiled_objects: # shape: Optional[Union[Point, PointList, Rect]] = None diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 330b273cdc..58b71b6c90 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -104,7 +104,6 @@ "PointList", "EMPTY_POINT_LIST", "AnchorPoint", - "IntRect", "Rect", "LRBT", "XYWH", @@ -113,7 +112,6 @@ "ViewportParams", "RectParams", "RectKwargs", - "RectList", "RGB", "RGBA", "RGBOrA", @@ -162,12 +160,6 @@ # 2. Allows type annotation to be cleaner, primarily for HitBox & subclasses EMPTY_POINT_LIST: PointList = tuple() - -IntRect = Union[Tuple[int, int, int, int], List[int]] # x, y, width, height -RectList = Union[Tuple[IntRect, ...], List[IntRect]] -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 @@ -183,7 +175,7 @@ class TiledObject(NamedTuple): - shape: Union[Point, PointList, IntRect] + shape: Union[Point, PointList, Tuple[int, int, int, int]] properties: Optional[Properties] = None name: Optional[str] = None type: Optional[str] = None From 6f5d94c6c4495c25be3a0967107bdc9ab7e9f3fd Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Tue, 28 May 2024 23:15:33 -0400 Subject: [PATCH 13/52] old Rect -> GUIRect --- arcade/gui/__init__.py | 4 +-- arcade/gui/ui_manager.py | 6 ++--- arcade/gui/widgets/__init__.py | 48 +++++++++++++++++----------------- arcade/gui/widgets/text.py | 4 +-- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/arcade/gui/__init__.py b/arcade/gui/__init__.py index 05a3fe896c..ddaab6523c 100644 --- a/arcade/gui/__init__.py +++ b/arcade/gui/__init__.py @@ -26,7 +26,7 @@ from arcade.gui.style import UIStyleBase, UIStyledWidget from arcade.gui.surface import Surface from arcade.gui.ui_manager import UIManager -from arcade.gui.widgets import UIDummy, Rect +from arcade.gui.widgets import UIDummy, GUIRect from arcade.gui.widgets import UIInteractiveWidget from arcade.gui.widgets import UILayout from arcade.gui.widgets import UISpace @@ -96,7 +96,7 @@ "UITextWidget", "UIWidget", "Surface", - "Rect", + "GUIRect", "NinePatchTexture", # Property classes "ListProperty", diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index 1522dd537f..8f2fa7dc9d 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -32,7 +32,7 @@ UITextMotionSelectEvent, ) from arcade.gui.surface import Surface -from arcade.gui.widgets import Rect, UIWidget +from arcade.gui.widgets import GUIRect, UIWidget from arcade.types import Point W = TypeVar("W", bound=UIWidget) @@ -397,8 +397,8 @@ def on_resize(self, width, height): self.trigger_render() @property - def rect(self) -> Rect: # type: ignore - return Rect(0, 0, *self.window.get_size()) + def rect(self) -> GUIRect: # type: ignore + return GUIRect(0, 0, *self.window.get_size()) def debug(self): """Walks through all widgets of a UIManager and prints out the rect""" diff --git a/arcade/gui/widgets/__init__.py b/arcade/gui/widgets/__init__.py index 7598b765ff..08a2c3eb96 100644 --- a/arcade/gui/widgets/__init__.py +++ b/arcade/gui/widgets/__init__.py @@ -31,7 +31,7 @@ __all__ = ["Surface", "UIDummy"] -class Rect(NamedTuple): +class GUIRect(NamedTuple): """ Representing a rectangle for GUI module. Rect is idempotent. @@ -44,10 +44,10 @@ class Rect(NamedTuple): width: float height: float - def move(self, dx: AsFloat = 0.0, dy: AsFloat = 0.0) -> "Rect": + def move(self, dx: AsFloat = 0.0, dy: AsFloat = 0.0) -> "GUIRect": """Returns new Rect which is moved by dx and dy""" x, y, width, height = self - return Rect(x + dx, y + dy, width, height) + return GUIRect(x + dx, y + dy, width, height) def collide_with_point(self, x: AsFloat, y: AsFloat) -> bool: """Return true if ``x`` and ``y`` are within this rect. @@ -70,7 +70,7 @@ def collide_with_point(self, x: AsFloat, y: AsFloat) -> bool: left, bottom, width, height = self return left <= x <= left + width and bottom <= y <= bottom + height - def scale(self, scale: float, rounding: Optional[Callable[..., float]] = floor) -> "Rect": + def scale(self, scale: float, rounding: Optional[Callable[..., float]] = floor) -> "GUIRect": """Return a new rect scaled relative to the origin. By default, the new rect's values are rounded down to whole @@ -87,20 +87,20 @@ def scale(self, scale: float, rounding: Optional[Callable[..., float]] = floor) """ x, y, width, height = self if rounding is not None: - return Rect( + return GUIRect( rounding(x * scale), rounding(y * scale), rounding(width * scale), rounding(height * scale), ) - return Rect( + return GUIRect( x * scale, y * scale, width * scale, height * scale, ) - def resize(self, width: float | None = None, height: float | None = None) -> "Rect": + def resize(self, width: float | None = None, height: float | None = None) -> "GUIRect": """Return a rect with a new width or height but same lower left. Fix x and y coordinate. @@ -109,7 +109,7 @@ def resize(self, width: float | None = None, height: float | None = None) -> "Re """ width = width if width is not None else self.width height = height if height is not None else self.height - return Rect(self.x, self.y, width, height) + return GUIRect(self.x, self.y, width, height) @property def size(self) -> Tuple[float, float]: @@ -158,54 +158,54 @@ def position(self) -> Point: """Bottom left coordinates""" return self.left, self.bottom - def align_top(self, value: float) -> "Rect": + def align_top(self, value: float) -> "GUIRect": """Returns new Rect, which is aligned to the top""" diff_y = value - self.top return self.move(dy=diff_y) - def align_bottom(self, value: float) -> "Rect": + def align_bottom(self, value: float) -> "GUIRect": """Returns new Rect, which is aligned to the bottom""" diff_y = value - self.bottom return self.move(dy=diff_y) - def align_left(self, value: float) -> "Rect": + def align_left(self, value: float) -> "GUIRect": """Returns new Rect, which is aligned to the left""" diff_x = value - self.left return self.move(dx=diff_x) - def align_right(self, value: AsFloat) -> "Rect": + def align_right(self, value: AsFloat) -> "GUIRect": """Returns new Rect, which is aligned to the right""" diff_x = value - self.right return self.move(dx=diff_x) - def align_center(self, center_x: AsFloat, center_y: AsFloat) -> "Rect": + def align_center(self, center_x: AsFloat, center_y: AsFloat) -> "GUIRect": """Returns new Rect, which is aligned to the center x and y""" diff_x = center_x - self.center_x diff_y = center_y - self.center_y return self.move(dx=diff_x, dy=diff_y) - def align_center_x(self, value: AsFloat) -> "Rect": + def align_center_x(self, value: AsFloat) -> "GUIRect": """Returns new Rect, which is aligned to the center_x""" diff_x = value - self.center_x return self.move(dx=diff_x) - def align_center_y(self, value: AsFloat) -> "Rect": + def align_center_y(self, value: AsFloat) -> "GUIRect": """Returns new Rect, which is aligned to the center_y""" diff_y = value - self.center_y return self.move(dy=diff_y) - def min_size(self, width: Optional[AsFloat] = None, height: Optional[AsFloat] = None) -> "Rect": + def min_size(self, width: Optional[AsFloat] = None, height: Optional[AsFloat] = None) -> "GUIRect": """ Sets the size to at least the given min values. """ - return Rect( + return GUIRect( self.x, self.y, max(width or 0.0, self.width), max(height or 0.0, self.height), ) - def max_size(self, width: Optional[AsFloat] = None, height: Optional[AsFloat] = None) -> "Rect": + def max_size(self, width: Optional[AsFloat] = None, height: Optional[AsFloat] = None) -> "GUIRect": """ Limits the size to the given max values. """ @@ -215,9 +215,9 @@ def max_size(self, width: Optional[AsFloat] = None, height: Optional[AsFloat] = if height is not None: h = min(height, h) - return Rect(x, y, w, h) + return GUIRect(x, y, w, h) - def union(self, rect: "Rect") -> "Rect": + def union(self, rect: "GUIRect") -> "GUIRect": """ Returns a new Rect that is the union of this rect and another. The union is the smallest rectangle that contains theses two rectangles. @@ -226,7 +226,7 @@ def union(self, rect: "Rect") -> "Rect": y = min(self.y, rect.y) right = max(self.right, rect.right) top = max(self.top, rect.top) - return Rect(x=x, y=y, width=right - x, height=top - y) + return GUIRect(x=x, y=y, width=right - x, height=top - y) W = TypeVar("W", bound="UIWidget") @@ -258,7 +258,7 @@ class UIWidget(EventDispatcher, ABC): :param style: not used """ - rect: Rect = Property(Rect(0, 0, 1, 1)) # type: ignore + rect: GUIRect = Property(GUIRect(0, 0, 1, 1)) # type: ignore visible: bool = Property(True) # type: ignore size_hint: Optional[Tuple[float, float]] = Property(None) # type: ignore @@ -291,7 +291,7 @@ def __init__( **kwargs, ): self._requires_render = True - self.rect = Rect(x, y, width, height) + self.rect = GUIRect(x, y, width, height) self.parent: Optional[Union[UIManager, UIWidget]] = None # Size hints are properties that can be used by layouts @@ -650,7 +650,7 @@ def content_height(self): @property def content_rect(self): - return Rect( + return GUIRect( self.left + self._border_width + self._padding_left, self.bottom + self._border_width + self._padding_bottom, self.content_width, diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index abdd7f0b6b..e84e577757 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -20,7 +20,7 @@ ) from arcade.gui.property import bind from arcade.gui.surface import Surface -from arcade.gui.widgets import UIWidget, Rect +from arcade.gui.widgets import UIWidget, GUIRect from arcade.gui.widgets.layout import UIAnchorLayout from arcade.types import RGBA255, Color, RGBOrA255 @@ -543,7 +543,7 @@ def fit_content(self): """ Set the width and height of the text area to contain the whole text. """ - self.rect = Rect( + self.rect = GUIRect( self.x, self.y, self.layout.content_width, From 4507a6b919049c6d0661bb7f4190a862099d9e73 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Tue, 28 May 2024 23:17:33 -0400 Subject: [PATCH 14/52] changelog --- PR.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PR.md b/PR.md index 8c5d61c54d..c6822cb8d4 100644 --- a/PR.md +++ b/PR.md @@ -1,4 +1,5 @@ # The Rect, Part II +## The Rect-oning and the Vec-oning - Added type aliases `Point2` and `Point3` - `Rect` @@ -9,3 +10,4 @@ - Added `Texture.draw_rect()` - Added `BasicSprite.rect` - Remove `IntRect`, `FloatRect`, `RectList` +- Rename the old `Rect` to `GUIRect` From 698cd9ee525a36c14e3cbc39d132611738a468ad Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Tue, 28 May 2024 23:26:45 -0400 Subject: [PATCH 15/52] accept tuples --- arcade/types/rect.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/arcade/types/rect.py b/arcade/types/rect.py index ab6b044cfd..f136ceb873 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -5,7 +5,7 @@ from pyglet.math import Vec2 from arcade.types.numbers import AsFloat -from arcade.types.vector_like import AnchorPoint +from arcade.types.vector_like import AnchorPoint, Point2 from arcade.utils import ReplacementWarning, warning @@ -133,9 +133,10 @@ def aspect_ratio(self) -> float: """Returns the ratio between the width and the height.""" return self.width / self.height - def at_position(self, position: Vec2) -> Rect: + def at_position(self, position: Point2) -> Rect: """Returns a new :py:class:`~arcade.types.rect.Rect` which is moved to put `position` at its center.""" - return XYWH(position.x, position.y, self.width, self.height) + x, y = position + return XYWH(x, y, self.width, self.height) def move(self, dx: AsFloat = 0.0, dy: AsFloat = 0.0) -> Rect: """ @@ -177,7 +178,7 @@ def scale(self, new_scale: AsFloat, anchor: Vec2 = AnchorPoint.CENTER) -> Rect: return LRBT(adjusted_left, adjusted_right, adjusted_bottom, adjusted_top) - def scale_axes(self, new_scale: Vec2, anchor: Vec2 = AnchorPoint.CENTER) -> Rect: + def scale_axes(self, new_scale: Point2, anchor: Vec2 = AnchorPoint.CENTER) -> Rect: """ Returns a new :py:class:`~arcade.types.rect.Rect` scaled by a factor of `new_scale.x` in the width @@ -186,10 +187,11 @@ def scale_axes(self, new_scale: Vec2, anchor: Vec2 = AnchorPoint.CENTER) -> Rect anchor_x = self.left + anchor.x * self.width anchor_y = self.bottom + anchor.y * self.height - adjusted_left = anchor_x + (self.left - anchor_x) * new_scale.x - adjusted_right = anchor_x + (self.right - anchor_x) * new_scale.x - adjusted_top = anchor_y + (self.top - anchor_y) * new_scale.y - adjusted_bottom = anchor_y + (self.bottom - anchor_y) * new_scale.y + nsx, nsy = new_scale + adjusted_left = anchor_x + (self.left - anchor_x) * nsx + adjusted_right = anchor_x + (self.right - anchor_x) * nsx + adjusted_top = anchor_y + (self.top - anchor_y) * nsy + adjusted_bottom = anchor_y + (self.bottom - anchor_y) * nsy return LRBT(adjusted_left, adjusted_right, adjusted_bottom, adjusted_top) @@ -209,9 +211,10 @@ def align_right(self, value: AsFloat) -> Rect: """Returns a new :py:class:`~arcade.types.rect.Rect`, which is aligned to the right at `value`.""" return LBWH(value - self.width, self.bottom, self.width, self.height) - def align_center(self, value: Vec2) -> Rect: + def align_center(self, value: Point2) -> Rect: """Returns a new :py:class:`~arcade.types.rect.Rect`, which is aligned to the center x and y at `value`.""" - return XYWH(value.x, value.y, self.width, self.height) + cx, cy = value + return XYWH(cx, cy, self.width, self.height) def align_x(self, value: AsFloat) -> Rect: """Returns a new :py:class:`~arcade.types.rect.Rect`, which is aligned to the x at `value`.""" @@ -345,23 +348,25 @@ def overlaps(self, other: Rect) -> bool: (other.height + self.height) / 2.0 > abs(self.y - other.y) ) - def point_in_rect(self, point: Vec2) -> bool: + def point_in_rect(self, point: Point2) -> bool: """Returns True if the given point is inside this rectangle.""" - return (self.left < point.x < self.right) and (self.bottom < point.y < self.top) + px, py = point + return (self.left < px < self.right) and (self.bottom < py < self.top) - def __contains__(self, point: Vec2) -> bool: + def __contains__(self, point: Point2) -> bool: """Returns the result of :py:meth:`point_in_rect` with a provided point.""" return self.point_in_rect(point) - def distance_from_bounds(self, point: Vec2) -> float: + def distance_from_bounds(self, point: Point2) -> float: """Returns the point's distance from the boundary of this rectangle.""" - diff = Vec2(point.x - self.x, point.y - self.y) + px, py = point + diff = Vec2(px - self.x, py - self.y) dx = abs(diff.x) - self.width / 2.0 dy = abs(diff.y) - self.height / 2.0 d = (max(dx, 0.0)**2 + max(dy, 0.0)**2)**0.5 + min(max(dx, dy), 0.0) return d - def point_on_bounds(self, point: Vec2, tolerance: float) -> bool: + def point_on_bounds(self, point: Point2, tolerance: float) -> bool: """Returns True if the given point is on the bounds of this rectangle within some tolerance.""" return abs(self.distance_from_bounds(point)) < tolerance From 0bf0d48a8c4491165831ea7ddb46210e854c1450 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Tue, 28 May 2024 23:27:19 -0400 Subject: [PATCH 16/52] changelog --- PR.md | 1 + 1 file changed, 1 insertion(+) diff --git a/PR.md b/PR.md index c6822cb8d4..41108bcdd9 100644 --- a/PR.md +++ b/PR.md @@ -5,6 +5,7 @@ - `Rect` - Added `Rect.distance_from_bounds()` - Added `point in rect` support for `Rect` + - Functions expecting `Vec2` now accept `Tuple[AsFloat, AsFloat]` - Improved docstrings - Added `Rect` and it's constructors, `Vec2`, and `Vec3` to top-level module - Added `Texture.draw_rect()` From 1a269edc085fc3c7606892d130217aad025bdf3b Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Tue, 28 May 2024 23:51:41 -0400 Subject: [PATCH 17/52] camera now take tuples if you feel like it --- PR.md | 4 +++- arcade/camera/camera_2d.py | 33 ++++++++++++++------------- arcade/camera/data_types.py | 17 ++++++++------ arcade/camera/default.py | 18 ++++++++------- arcade/camera/orthographic.py | 16 +++++++------ arcade/camera/perspective.py | 16 +++++++------ arcade/camera/projection_functions.py | 5 ++-- arcade/camera/static.py | 31 +++++++++++++------------ 8 files changed, 78 insertions(+), 62 deletions(-) diff --git a/PR.md b/PR.md index 41108bcdd9..0c46f661f4 100644 --- a/PR.md +++ b/PR.md @@ -1,12 +1,14 @@ # The Rect, Part II ## The Rect-oning and the Vec-oning -- Added type aliases `Point2` and `Point3` - `Rect` - Added `Rect.distance_from_bounds()` - Added `point in rect` support for `Rect` - Functions expecting `Vec2` now accept `Tuple[AsFloat, AsFloat]` - Improved docstrings +- Added type aliases `Point2` and `Point3` +- Camera + - All camera functions now take `Point`, `Point2`, or `Point3` where points are expected - Added `Rect` and it's constructors, `Vec2`, and `Vec3` to top-level module - Added `Texture.draw_rect()` - Added `BasicSprite.rect` diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 26bce68325..529ea475ba 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -13,6 +13,7 @@ from arcade.gl import Framebuffer from pyglet.math import Vec2 +from arcade.types.vector_like import Point2 from arcade.window_commands import get_window if TYPE_CHECKING: @@ -234,12 +235,12 @@ def projection_data(self) -> OrthographicProjectionData: return self._projection_data @property - def position(self) -> Tuple[float, float]: + def position(self) -> Vec2: """The 2D world position of the camera along the X and Y axes.""" - return self._camera_data.position[0], self._camera_data.position[1] + return Vec2(self._camera_data.position[0], self._camera_data.position[1]) @position.setter - def position(self, _pos: Tuple[float, float]) -> None: + def position(self, _pos: Point2) -> None: self._camera_data.position = (_pos[0], _pos[1], self._camera_data.position[2]) # top_left @@ -255,7 +256,7 @@ def top_left(self) -> Vec2: return Vec2(pos[0] + up[0] * top + up[1] * left, pos[1] + up[1] * top - up[0] * left) @top_left.setter - def top_left(self, new_corner: Tuple[float, float]): + def top_left(self, new_corner: Point2): up = self._camera_data.up top = self.top @@ -276,7 +277,7 @@ def top_center(self) -> Vec2: return Vec2(pos[0] + up[0] * top, pos[1] + up[1] * top) @top_center.setter - def top_center(self, new_top: Tuple[float, float]): + def top_center(self, new_top: Point2): up = self._camera_data.up top = self.top @@ -295,7 +296,7 @@ def top_right(self) -> Vec2: return Vec2(pos[0] + up[0] * top + up[1] * right, pos[1] + up[1] * top - up[0] * right) @top_right.setter - def top_right(self, new_corner: Tuple[float, float]): + def top_right(self, new_corner: Point2): up = self._camera_data.up top = self.top @@ -318,7 +319,7 @@ def bottom_right(self) -> Vec2: return Vec2(pos[0] + up[0] * bottom + up[1] * right, pos[1] + up[1] * bottom - up[0] * right) @bottom_right.setter - def bottom_right(self, new_corner: Tuple[float, float]): + def bottom_right(self, new_corner: Point2): up = self._camera_data.up bottom = self.bottom @@ -340,7 +341,7 @@ def bottom_center(self) -> Vec2: return Vec2(pos[0] - up[0] * bottom, pos[1] - up[1] * bottom) @bottom_center.setter - def bottom_center(self, new_bottom: Tuple[float, float]): + def bottom_center(self, new_bottom: Point2): up = self._camera_data.up bottom = self.bottom @@ -359,7 +360,7 @@ def bottom_left(self) -> Vec2: return Vec2(pos[0] + up[0] * bottom + up[1] * left, pos[1] + up[1] * bottom - up[0] * left) @bottom_left.setter - def bottom_left(self, new_corner: Tuple[float, float]): + def bottom_left(self, new_corner: Point2): up = self._camera_data.up bottom = self.bottom @@ -380,7 +381,7 @@ def center_right(self) -> Vec2: return Vec2(pos[0] + up[1] * right, pos[1] - up[0] * right) @center_right.setter - def center_right(self, new_right: Tuple[float, float]): + def center_right(self, new_right: Point2): up = self._camera_data.up right = self.right self.position = new_right[0] - up[1] * right, new_right[1] + up[0] * right @@ -395,12 +396,12 @@ def center_left(self) -> Vec2: return Vec2(pos[0] + up[1] * left, pos[1] - up[0] * left) @center_left.setter - def center_left(self, new_left: Tuple[float, float]): + def center_left(self, new_left: Point2): up = self._camera_data.up left = self.left self.position = new_left[0] - up[1] * left, new_left[1] - up[0] * left - def point_in_view(self, point: Tuple[float, float]) -> bool: + def point_in_view(self, point: Point2) -> bool: """ Take a 2D point in the world, and return whether the point is inside the visible area of the camera. """ @@ -694,7 +695,7 @@ def viewport_top(self, _top: int) -> None: self._projection_data.viewport[2], self._projection_data.viewport[3]) @property - def up(self) -> Tuple[float, float]: + def up(self) -> Vec2: """ A 2D vector which describes what is mapped to the +Y direction on screen. @@ -702,10 +703,10 @@ def up(self) -> Tuple[float, float]: The base vector is 3D, but the simplified camera only provides a 2D view. """ - return self._camera_data.up[0], self._camera_data.up[1] + return Vec2(self._camera_data.up[0], self._camera_data.up[1]) @up.setter - def up(self, _up: Tuple[float, float]) -> None: + def up(self, _up: Point2) -> None: """ Set the 2D vector which describes what is mapped to the +Y direction on screen. @@ -818,7 +819,7 @@ def activate(self) -> Generator[Self, None, None]: previous_framebuffer.use() previous_projection.use() - def project(self, world_coordinate: Tuple[float, ...]) -> Tuple[float, float]: + def project(self, world_coordinate: Tuple[float, ...]) -> Vec2: """ Take a Vec2 or Vec3 of coordinates and return the related screen coordinate """ diff --git a/arcade/camera/data_types.py b/arcade/camera/data_types.py index 0530742c60..cf0709a745 100644 --- a/arcade/camera/data_types.py +++ b/arcade/camera/data_types.py @@ -8,7 +8,10 @@ from contextlib import contextmanager from typing_extensions import Self -from pyglet.math import Vec3 +from pyglet.math import Vec2, Vec3 + +from arcade.types import Point +from arcade.types.vector_like import Point2, Point3 __all__ = [ @@ -54,9 +57,9 @@ class CameraData: __slots__ = ("position", "up", "forward", "zoom") def __init__(self, - position: Tuple[float, float, float] = (0.0, 0.0, 0.0), - up: Tuple[float, float, float] = (0.0, 1.0, 0.0), - forward: Tuple[float, float, float] = (0.0, 0.0, -1.0), + position: Point3 = (0.0, 0.0, 0.0), + up: Point3 = (0.0, 1.0, 0.0), + forward: Point3 = (0.0, 0.0, -1.0), zoom: float = 1.0): # View matrix data @@ -293,15 +296,15 @@ def use(self) -> None: def activate(self) -> Generator[Self, None, None]: ... - def project(self, world_coordinate: Tuple[float, ...]) -> Tuple[float, float]: + def project(self, world_coordinate: Point) -> Vec2: """ Take a Vec2 or Vec3 of coordinates and return the related screen coordinate """ ... def unproject(self, - screen_coordinate: Tuple[float, float], - depth: Optional[float] = None) -> Tuple[float, float, float]: + screen_coordinate: Point2, + depth: Optional[float] = None) -> Vec3: """ Take in a pixel coordinate and return the associated world coordinate diff --git a/arcade/camera/default.py b/arcade/camera/default.py index 0c16d51e3d..a1b6b95b30 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -2,8 +2,10 @@ from typing_extensions import Self from contextlib import contextmanager -from pyglet.math import Mat4 +from pyglet.math import Mat4, Vec2, Vec3 +from arcade.types import Point +from arcade.types.vector_like import Point2 from arcade.window_commands import get_window if TYPE_CHECKING: from arcade.context import ArcadeContext @@ -76,27 +78,27 @@ def activate(self) -> Generator[Self, None, None]: finally: previous.use() - def project(self, world_coordinate: Tuple[float, ...]) -> Tuple[float, float]: + def project(self, world_coordinate: Point) -> Vec2: """ Take a Vec2 or Vec3 of coordinates and return the related screen coordinate """ - return world_coordinate[0], world_coordinate[1] + return Vec2(world_coordinate[0], world_coordinate[1]) def unproject( self, - screen_coordinate: Tuple[float, float], - depth: Optional[float] = None) -> Tuple[float, float, float]: + screen_coordinate: Point2, + depth: Optional[float] = None) -> Vec3: """ Map the screen pos to screen_coordinates. Due to the nature of viewport projector this does not do anything. """ - return screen_coordinate[0], screen_coordinate[1], depth or 0.0 + return Vec3(screen_coordinate[0], screen_coordinate[1], depth or 0.0) def map_screen_to_world_coordinate( self, - screen_coordinate: Tuple[float, float], - depth: Optional[float] = None) -> Tuple[float, float, float]: + screen_coordinate: Point2, + depth: Optional[float] = None) -> Vec3: """ Alias of ViewportProjector.unproject() for typing. """ diff --git a/arcade/camera/orthographic.py b/arcade/camera/orthographic.py index 07e239710d..889d71cbf9 100644 --- a/arcade/camera/orthographic.py +++ b/arcade/camera/orthographic.py @@ -1,8 +1,8 @@ -from typing import Optional, Tuple, Generator, TYPE_CHECKING +from typing import Optional, Generator, TYPE_CHECKING from contextlib import contextmanager from typing_extensions import Self -from pyglet.math import Mat4, Vec3 +from pyglet.math import Mat4, Vec3, Vec2 from arcade.camera.data_types import Projector, CameraData, OrthographicProjectionData from arcade.camera.projection_functions import ( @@ -12,6 +12,8 @@ unproject_orthographic ) +from arcade.types import Point +from arcade.types.vector_like import Point2 from arcade.window_commands import get_window if TYPE_CHECKING: from arcade import Window @@ -134,7 +136,7 @@ def activate(self) -> Generator[Self, None, None]: finally: previous_projector.use() - def project(self, world_coordinate: Tuple[float, ...]) -> Tuple[float, float]: + def project(self, world_coordinate: Point) -> Vec2: """ Take a Vec2 or Vec3 of coordinates and return the related screen coordinate """ @@ -152,11 +154,11 @@ def project(self, world_coordinate: Tuple[float, ...]) -> Tuple[float, float]: _view, _projection, ) - return pos.x, pos.y + return pos def unproject(self, - screen_coordinate: Tuple[float, float], - depth: Optional[float] = None) -> Tuple[float, float, float]: + screen_coordinate: Point2, + depth: Optional[float] = None) -> Vec3: """ Take in a pixel coordinate from within the range of the window size and returns @@ -181,4 +183,4 @@ def unproject(self, depth or 0.0 ) - return pos.x, pos.y, pos.z + return pos diff --git a/arcade/camera/perspective.py b/arcade/camera/perspective.py index 3f7f33910d..af6d236967 100644 --- a/arcade/camera/perspective.py +++ b/arcade/camera/perspective.py @@ -13,6 +13,8 @@ unproject_perspective ) +from arcade.types import Point +from arcade.types.vector_like import Point2 from arcade.window_commands import get_window if TYPE_CHECKING: from arcade import Window @@ -133,7 +135,7 @@ def use(self) -> None: self._window.projection = _projection self._window.view = _view - def project(self, world_coordinate: Tuple[float, ...]) -> Tuple[float, float]: + def project(self, world_coordinate: Point) -> Vec2: """ Take a Vec2 or Vec3 of coordinates and return the related screen coordinate """ @@ -153,11 +155,11 @@ def project(self, world_coordinate: Tuple[float, ...]) -> Tuple[float, float]: _view, _projection ) - return pos.x, pos.y + return pos def unproject(self, - screen_coordinate: Tuple[float, float], - depth: Optional[float] = None) -> Tuple[float, float, float]: + screen_coordinate: Point2, + depth: Optional[float] = None) -> Vec3: """ Take in a pixel coordinate from within the range of the window size and returns @@ -183,12 +185,12 @@ def unproject(self, _view, _projection, depth ) - return pos.x, pos.y, pos.z + return pos def map_screen_to_world_coordinate( self, - screen_coordinate: Tuple[float, float], - depth: Optional[float] = None) -> Tuple[float, float, float]: + screen_coordinate: Point2, + depth: Optional[float] = None) -> Vec3: """ Alias of PerspectiveProjector.unproject() for typing. """ diff --git a/arcade/camera/projection_functions.py b/arcade/camera/projection_functions.py index 9874075f4a..94bded6cf3 100644 --- a/arcade/camera/projection_functions.py +++ b/arcade/camera/projection_functions.py @@ -3,6 +3,7 @@ from pyglet.math import Vec2, Vec3, Vec4, Mat4 from arcade.camera.data_types import CameraData, PerspectiveProjectionData, OrthographicProjectionData +from arcade.types.vector_like import Point2 def generate_view_matrix(camera_data: CameraData) -> Mat4: @@ -114,7 +115,7 @@ def project_orthographic(world_coordinate: Vec3, return Vec2(screen_coordinate_x, screen_coordinate_y) -def unproject_orthographic(screen_coordinate: Union[Vec2, Tuple[float, float]], +def unproject_orthographic(screen_coordinate: Point2, viewport: Tuple[int, int, int, int], view_matrix: Mat4, projection_matrix: Mat4, depth: Optional[float] = None) -> Vec3: @@ -147,7 +148,7 @@ def project_perspective(world_coordinate: Vec3, return Vec2(screen_coordinate_x, screen_coordinate_y) -def unproject_perspective(screen_coordinate: Union[Vec2, Tuple[float, float]], +def unproject_perspective(screen_coordinate: Point2, viewport: Tuple[int, int, int, int], view_matrix: Mat4, projection_matrix: Mat4, depth: Optional[float] = None) -> Vec3: diff --git a/arcade/camera/static.py b/arcade/camera/static.py index 9abded185e..84a100c2cf 100644 --- a/arcade/camera/static.py +++ b/arcade/camera/static.py @@ -14,6 +14,8 @@ unproject_perspective ) +from arcade.types import Point +from arcade.types.vector_like import Point2, Point3 from arcade.window_commands import get_window from pyglet.math import Mat4, Vec3, Vec2 @@ -56,7 +58,7 @@ def activate(self) -> Generator[_StaticCamera, None, None]: finally: prev.use() - def project(self, world_coordinate: Tuple[float, ...]) -> Tuple[float, float]: + def project(self, world_coordinate: Point) -> Vec2: """ Take a Vec2 or Vec3 of coordinates and return the related screen coordinate """ @@ -67,11 +69,11 @@ def project(self, world_coordinate: Tuple[float, ...]) -> Tuple[float, float]: Vec3(world_coordinate[0], world_coordinate[1], world_coordinate[2]), self._viewport, self._view, self._projection ) - return pos.x, pos.y + return pos def unproject(self, - screen_coordinate: Tuple[float, float], - depth: Optional[float] = None) -> Tuple[float, float, float]: + screen_coordinate: Point2, + depth: Optional[float] = None) -> Vec3: """ Take in a pixel coordinate from within the range of the window size and returns @@ -93,7 +95,8 @@ def unproject(self, Vec2(screen_coordinate[0], screen_coordinate[1]), self._viewport, self._view, self._projection, depth ) - return pos.x, pos.y, pos.z + return pos + def static_from_orthographic( view: CameraData, @@ -129,9 +132,9 @@ def static_from_raw_orthographic( projection: Tuple[float, float, float, float], near: float = -100.0, far: float = 100.0, zoom: float = 1.0, - position: Tuple[float, float, float] = (0.0, 0.0, 0.0), - up: Tuple[float, float, float] = (0.0, 1.0, 0.0), - forward: Tuple[float, float, float] = (0.0, 0.0, -1.0), + position: Point3 = (0.0, 0.0, 0.0), + up: Point3 = (0.0, 1.0, 0.0), + forward: Point3 = (0.0, 0.0, -1.0), viewport: Optional[Tuple[int, int, int, int]] = None, *, window: Optional[Window] = None @@ -152,9 +155,9 @@ def static_from_raw_perspective( aspect: float, fov: float, near: float = -100.0, far: float = 100.0, zoom: float = 1.0, - position: Tuple[float, float, float] = (0.0, 0.0, 0.0), - up: Tuple[float, float, float] = (0.0, 1.0, 0.0), - forward: Tuple[float, float, float] = (0.0, 0.0, -1.0), + position: Point3 = (0.0, 0.0, 0.0), + up: Point3 = (0.0, 1.0, 0.0), + forward: Point3 = (0.0, 0.0, -1.0), viewport: Optional[Tuple[int, int, int, int]] = None, *, window: Optional[Window] = None @@ -175,9 +178,9 @@ def static_from_matrices( view: Mat4, projection: Mat4, viewport: Optional[Tuple[int, int, int, int]], *, - window: Optional[Window]=None, - project_method: Optional[Callable[[Vec3, Tuple[int, int, int, int], Mat4, Mat4], Vec2]]=None, - unproject_method: Optional[Callable[[Vec2, Tuple[int, int, int, int], Mat4, Mat4, Optional[float]], Vec3]]=None + window: Optional[Window] = None, + project_method: Optional[Callable[[Vec3, Tuple[int, int, int, int], Mat4, Mat4], Vec2]] = None, + unproject_method: Optional[Callable[[Vec2, Tuple[int, int, int, int], Mat4, Mat4, Optional[float]], Vec3]] = None ) -> _StaticCamera: return _StaticCamera(view, projection, viewport, window=window, project_method=project_method, unproject_method=unproject_method) From 4856a4e28c5b998ce5ea30ab05cf53a8e6a155e3 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Wed, 29 May 2024 18:52:44 +1200 Subject: [PATCH 18/52] Camera vectified and tests corrected. Linting is semi done. Pyright is being an ass --- arcade/camera/camera_2d.py | 49 ++++++++++++------- arcade/camera/data_types.py | 8 ++- arcade/camera/default.py | 19 +++---- arcade/camera/orthographic.py | 25 +++------- arcade/camera/perspective.py | 39 +++++---------- arcade/camera/projection_functions.py | 49 ++++++++++--------- arcade/camera/static.py | 28 +++++------ arcade/draw_commands.py | 10 ++-- arcade/drawing_support.py | 6 ++- arcade/gui/experimental/scroll_area.py | 3 +- arcade/gui/surface.py | 2 +- arcade/hitbox/base.py | 4 +- arcade/math.py | 14 +++--- arcade/paths.py | 12 ++--- arcade/sprite/base.py | 13 +++-- arcade/sprite/sprite.py | 6 +-- arcade/text.py | 10 ++-- arcade/texture/loading.py | 2 +- arcade/types/__init__.py | 7 ++- arcade/types/rect.py | 5 +- .../camera/test_orthographic_projector.py | 34 +++++++------ .../unit/camera/test_perspective_projector.py | 22 ++++----- tests/unit/gui/test_layouting_boxlayout.py | 28 +++++------ tests/unit/gui/test_layouting_examples.py | 18 +++---- tests/unit/gui/test_layouting_gridlayout.py | 22 ++++----- tests/unit/gui/test_rect.py | 40 +++++++-------- tests/unit/gui/test_uilabel.py | 8 +-- tests/unit/sprite/test_sprite_collision.py | 10 ++-- 28 files changed, 240 insertions(+), 253 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 529ea475ba..1cbbc95861 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -11,8 +11,9 @@ ZeroProjectionDimension ) from arcade.gl import Framebuffer -from pyglet.math import Vec2 +from pyglet.math import Vec2, Vec3 +from arcade.types import Point from arcade.types.vector_like import Point2 from arcade.window_commands import get_window @@ -241,7 +242,9 @@ def position(self) -> Vec2: @position.setter def position(self, _pos: Point2) -> None: - self._camera_data.position = (_pos[0], _pos[1], self._camera_data.position[2]) + + x, y = _pos + self._camera_data.position = (x, y, self._camera_data.position[2]) # top_left @property @@ -262,9 +265,10 @@ def top_left(self, new_corner: Point2): top = self.top left = self.left + x, y = new_corner self.position = ( - new_corner[0] - up[0] * top - up[1] * left, - new_corner[1] - up[0] * top + up[0] * left + x - up[0] * top - up[1] * left, + y - up[0] * top + up[0] * left ) # top_center @@ -281,7 +285,8 @@ def top_center(self, new_top: Point2): up = self._camera_data.up top = self.top - self.position = new_top[0] - up[0] * top, new_top[1] - up[1] * top + x, y = new_top + self.position = x - up[0] * top, y - up[1] * top # top_right @property @@ -302,9 +307,10 @@ def top_right(self, new_corner: Point2): top = self.top right = self.right + x, y = new_corner self.position = ( - new_corner[0] - up[0] * top - up[1] * right, - new_corner[1] - up[1] * top + up[0] * right + x - up[0] * top - up[1] * right, + y - up[1] * top + up[0] * right ) # bottom_right @@ -325,9 +331,10 @@ def bottom_right(self, new_corner: Point2): bottom = self.bottom right = self.right + x, y = new_corner self.position = ( - new_corner[0] - up[0] * bottom - up[1] * right, - new_corner[1] - up[1] * bottom + up[0] * right + x - up[0] * bottom - up[1] * right, + y - up[1] * bottom + up[0] * right ) # bottom_center @@ -345,7 +352,8 @@ def bottom_center(self, new_bottom: Point2): up = self._camera_data.up bottom = self.bottom - self.position = new_bottom[0] - up[0] * bottom, new_bottom[1] - up[0] * bottom + x, y = new_bottom + self.position = x - up[0] * bottom, y - up[0] * bottom # bottom_left @property @@ -366,9 +374,10 @@ def bottom_left(self, new_corner: Point2): bottom = self.bottom left = self.left + x, y = new_corner self.position = ( - new_corner[0] - up[0] * bottom - up[1] * left, - new_corner[1] - up[1] * bottom + up[0] * left + x - up[0] * bottom - up[1] * left, + y - up[1] * bottom + up[0] * left ) # center_right @@ -384,7 +393,9 @@ def center_right(self) -> Vec2: def center_right(self, new_right: Point2): up = self._camera_data.up right = self.right - self.position = new_right[0] - up[1] * right, new_right[1] + up[0] * right + + x, y = new_right + self.position = x - up[1] * right, y + up[0] * right # center_left @property @@ -399,7 +410,9 @@ def center_left(self) -> Vec2: def center_left(self, new_left: Point2): up = self._camera_data.up left = self.left - self.position = new_left[0] - up[1] * left, new_left[1] - up[0] * left + + x, y = new_left + self.position = x - up[1] * left, y - up[0] * left def point_in_view(self, point: Point2) -> bool: """ @@ -819,15 +832,13 @@ def activate(self) -> Generator[Self, None, None]: previous_framebuffer.use() previous_projection.use() - def project(self, world_coordinate: Tuple[float, ...]) -> Vec2: + def project(self, world_coordinate: Point) -> Vec2: """ Take a Vec2 or Vec3 of coordinates and return the related screen coordinate """ return self._ortho_projector.project(world_coordinate) - def unproject(self, - screen_coordinate: Tuple[float, float], - depth: Optional[float] = None) -> Tuple[float, float, float]: + def unproject(self, screen_coordinate: Point) -> Vec3: """ Take in a pixel coordinate from within the range of the window size and returns @@ -845,4 +856,4 @@ def unproject(self, of the camera. """ - return self._ortho_projector.unproject(screen_coordinate, depth) + return self._ortho_projector.unproject(screen_coordinate) diff --git a/arcade/camera/data_types.py b/arcade/camera/data_types.py index cf0709a745..5af6e1423c 100644 --- a/arcade/camera/data_types.py +++ b/arcade/camera/data_types.py @@ -4,14 +4,14 @@ wide usage throughout Arcade's camera code. """ from __future__ import annotations -from typing import Protocol, Tuple, Optional, Generator +from typing import Protocol, Tuple, Generator from contextlib import contextmanager from typing_extensions import Self from pyglet.math import Vec2, Vec3 from arcade.types import Point -from arcade.types.vector_like import Point2, Point3 +from arcade.types.vector_like import Point3 __all__ = [ @@ -302,9 +302,7 @@ def project(self, world_coordinate: Point) -> Vec2: """ ... - def unproject(self, - screen_coordinate: Point2, - depth: Optional[float] = None) -> Vec3: + def unproject(self, screen_coordinate: Point) -> Vec3: """ Take in a pixel coordinate and return the associated world coordinate diff --git a/arcade/camera/default.py b/arcade/camera/default.py index a1b6b95b30..fc404755e6 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -5,7 +5,6 @@ from pyglet.math import Mat4, Vec2, Vec3 from arcade.types import Point -from arcade.types.vector_like import Point2 from arcade.window_commands import get_window if TYPE_CHECKING: from arcade.context import ArcadeContext @@ -82,27 +81,21 @@ def project(self, world_coordinate: Point) -> Vec2: """ Take a Vec2 or Vec3 of coordinates and return the related screen coordinate """ - return Vec2(world_coordinate[0], world_coordinate[1]) + x, y, *z = world_coordinate + return Vec2(x, y) def unproject( self, - screen_coordinate: Point2, - depth: Optional[float] = None) -> Vec3: + screen_coordinate: Point) -> Vec3: """ Map the screen pos to screen_coordinates. Due to the nature of viewport projector this does not do anything. """ - return Vec3(screen_coordinate[0], screen_coordinate[1], depth or 0.0) + x, y, *z = screen_coordinate + z = 0.0 if not z else z[0] - def map_screen_to_world_coordinate( - self, - screen_coordinate: Point2, - depth: Optional[float] = None) -> Vec3: - """ - Alias of ViewportProjector.unproject() for typing. - """ - return self.unproject(screen_coordinate, depth) + return Vec3(x, y, z) # As this class is only supposed to be used internally diff --git a/arcade/camera/orthographic.py b/arcade/camera/orthographic.py index 889d71cbf9..19e2d5e9e7 100644 --- a/arcade/camera/orthographic.py +++ b/arcade/camera/orthographic.py @@ -13,7 +13,6 @@ ) from arcade.types import Point -from arcade.types.vector_like import Point2 from arcade.window_commands import get_window if TYPE_CHECKING: from arcade import Window @@ -140,25 +139,15 @@ def project(self, world_coordinate: Point) -> Vec2: """ Take a Vec2 or Vec3 of coordinates and return the related screen coordinate """ - if len(world_coordinate) > 2: - z = world_coordinate[2] - else: - z = 0.0 - x, y = world_coordinate[0], world_coordinate[1] - _projection = generate_orthographic_matrix(self._projection, self._view.zoom) _view = generate_view_matrix(self._view) - pos = project_orthographic( - Vec3(x, y, z), self.projection.viewport, + return project_orthographic( + world_coordinate, self.projection.viewport, _view, _projection, ) - return pos - - def unproject(self, - screen_coordinate: Point2, - depth: Optional[float] = None) -> Vec3: + def unproject(self, screen_coordinate: Point) -> Vec3: """ Take in a pixel coordinate from within the range of the window size and returns @@ -173,14 +162,12 @@ def unproject(self, Returns: A 3D vector in world space. """ + _projection = generate_orthographic_matrix(self._projection, self._view.zoom) _view = generate_view_matrix(self._view) - pos = unproject_orthographic( + return unproject_orthographic( screen_coordinate, self.projection.viewport, - _view, _projection, - depth or 0.0 + _view, _projection ) - - return pos diff --git a/arcade/camera/perspective.py b/arcade/camera/perspective.py index af6d236967..fbcee0715f 100644 --- a/arcade/camera/perspective.py +++ b/arcade/camera/perspective.py @@ -1,9 +1,9 @@ -from typing import Optional, Tuple, Generator, TYPE_CHECKING +from typing import Optional, Generator, TYPE_CHECKING from typing_extensions import Self from contextlib import contextmanager from math import tan, radians -from pyglet.math import Mat4, Vec3 +from pyglet.math import Mat4, Vec3, Vec2 from arcade.camera.data_types import Projector, CameraData, PerspectiveProjectionData from arcade.camera.projection_functions import ( @@ -14,7 +14,6 @@ ) from arcade.types import Point -from arcade.types.vector_like import Point2 from arcade.window_commands import get_window if TYPE_CHECKING: from arcade import Window @@ -139,12 +138,9 @@ def project(self, world_coordinate: Point) -> Vec2: """ Take a Vec2 or Vec3 of coordinates and return the related screen coordinate """ - if len(world_coordinate) < 2: - z = (0.5 * self._projection.viewport[3] / tan( - radians(0.5 * self._projection.fov / self._view.zoom))) - else: - z = world_coordinate[2] - x, y = world_coordinate[0], world_coordinate[1] + x, y, *z = world_coordinate + z = (0.5 * self._projection.viewport[3] / tan( + radians(0.5 * self._projection.fov / self._view.zoom))) if not z else z[0] _projection = generate_perspective_matrix(self._projection, self._view.zoom) _view = generate_view_matrix(self._view) @@ -157,9 +153,7 @@ def project(self, world_coordinate: Point) -> Vec2: return pos - def unproject(self, - screen_coordinate: Point2, - depth: Optional[float] = None) -> Vec3: + def unproject(self, screen_coordinate: Point) -> Vec3: """ Take in a pixel coordinate from within the range of the window size and returns @@ -167,31 +161,22 @@ def unproject(self, Essentially reverses the effects of the projector. + # TODO: UPDATE Args: screen_coordinate: A 2D position in pixels from the bottom left of the screen. This should ALWAYS be in the range of 0.0 - screen size. - depth: The depth of the query Returns: A 3D vector in world space. """ - depth = depth or (0.5 * self._projection.viewport[3] / tan( - radians(0.5 * self._projection.fov / self._view.zoom))) + x, y, *z = screen_coordinate + z = (0.5 * self._projection.viewport[3] / tan( + radians(0.5 * self._projection.fov / self._view.zoom))) if not z else z[0] _projection = generate_perspective_matrix(self._projection, self._view.zoom) _view = generate_view_matrix(self._view) pos = unproject_perspective( - screen_coordinate, self.projection.viewport, - _view, _projection, - depth + Vec3(x, y, z), self.projection.viewport, + _view, _projection ) return pos - - def map_screen_to_world_coordinate( - self, - screen_coordinate: Point2, - depth: Optional[float] = None) -> Vec3: - """ - Alias of PerspectiveProjector.unproject() for typing. - """ - return self.unproject(screen_coordinate, depth) diff --git a/arcade/camera/projection_functions.py b/arcade/camera/projection_functions.py index 94bded6cf3..65a860a09d 100644 --- a/arcade/camera/projection_functions.py +++ b/arcade/camera/projection_functions.py @@ -1,9 +1,9 @@ from math import tan, pi -from typing import Optional, Union, Tuple +from typing import Tuple from pyglet.math import Vec2, Vec3, Vec4, Mat4 from arcade.camera.data_types import CameraData, PerspectiveProjectionData, OrthographicProjectionData -from arcade.types.vector_like import Point2 +from arcade.types import Point, Point3 def generate_view_matrix(camera_data: CameraData) -> Mat4: @@ -25,7 +25,7 @@ def generate_view_matrix(camera_data: CameraData) -> Mat4: )) -def generate_orthographic_matrix(perspective_data: OrthographicProjectionData, zoom: float = 1.0): +def generate_orthographic_matrix(perspective_data: OrthographicProjectionData, zoom: float = 1.0) -> Mat4: """ Using the OrthographicProjectionData a projection matrix is generated where the size of an object is not affected by depth. @@ -63,7 +63,7 @@ def generate_orthographic_matrix(perspective_data: OrthographicProjectionData, z )) -def generate_perspective_matrix(perspective_data: PerspectiveProjectionData, zoom: float = 1.0): +def generate_perspective_matrix(perspective_data: PerspectiveProjectionData, zoom: float = 1.0) -> Mat4: """ Using the OrthographicProjectionData a projection matrix is generated where the size of the objects is not affected by depth. @@ -96,14 +96,11 @@ def generate_perspective_matrix(perspective_data: PerspectiveProjectionData, zoo )) -def project_orthographic(world_coordinate: Vec3, +def project_orthographic(world_coordinate: Point, viewport: Tuple[int, int, int, int], view_matrix: Mat4, projection_matrix: Mat4) -> Vec2: - if len(world_coordinate) > 2: - z = world_coordinate[2] - else: - z = 0.0 - x, y = world_coordinate[0], world_coordinate[1] + x, y, *z = world_coordinate + z = 0.0 if not z else z[0] world_position = Vec4(x, y, z, 1.0) @@ -115,10 +112,13 @@ def project_orthographic(world_coordinate: Vec3, return Vec2(screen_coordinate_x, screen_coordinate_y) -def unproject_orthographic(screen_coordinate: Point2, +def unproject_orthographic(screen_coordinate: Point, viewport: Tuple[int, int, int, int], - view_matrix: Mat4, projection_matrix: Mat4, - depth: Optional[float] = None) -> Vec3: + view_matrix: Mat4, projection_matrix: Mat4 + ) -> Vec3: + x, y, *z = screen_coordinate + z = 0.0 if not z else z[0] + screen_x = 2.0 * (screen_coordinate[0] - viewport[0]) / viewport[2] - 1 screen_y = 2.0 * (screen_coordinate[1] - viewport[1]) / viewport[3] - 1 @@ -126,15 +126,17 @@ def unproject_orthographic(screen_coordinate: Point2, _view = ~view_matrix _unprojected_position = _projection @ Vec4(screen_x, screen_y, 0.0, 1.0) - _world_position = _view @ Vec4(_unprojected_position.x, _unprojected_position.y, depth or 0.0, 1.0) + _world_position = _view @ Vec4(_unprojected_position.x, _unprojected_position.y, z, 1.0) return Vec3(_world_position.x, _world_position.y, _world_position.z) -def project_perspective(world_coordinate: Vec3, +def project_perspective(world_coordinate: Point3, viewport: Tuple[int, int, int, int], view_matrix: Mat4, projection_matrix: Mat4) -> Vec2: - world_position = Vec4(world_coordinate.x, world_coordinate.y, world_coordinate.z, 1.0) + x, y, z = world_coordinate + + world_position = Vec4(x, y, z, 1.0) semi_projected_position = projection_matrix @ view_matrix @ world_position div_val = semi_projected_position.w @@ -148,21 +150,22 @@ def project_perspective(world_coordinate: Vec3, return Vec2(screen_coordinate_x, screen_coordinate_y) -def unproject_perspective(screen_coordinate: Point2, +def unproject_perspective(screen_coordinate: Point, viewport: Tuple[int, int, int, int], - view_matrix: Mat4, projection_matrix: Mat4, - depth: Optional[float] = None) -> Vec3: - depth = depth or 1.0 + view_matrix: Mat4, projection_matrix: Mat4 + ) -> Vec3: + x, y, *z = screen_coordinate + z = 1.0 if not z else z[0] screen_x = 2.0 * (screen_coordinate[0] - viewport[0]) / viewport[2] - 1 screen_y = 2.0 * (screen_coordinate[1] - viewport[1]) / viewport[3] - 1 - screen_x *= depth - screen_y *= depth + screen_x *= z + screen_y *= z projected_position = Vec4(screen_x, screen_y, 1.0, 1.0) view_position = ~projection_matrix @ projected_position - world_position = ~view_matrix @ Vec4(view_position.x, view_position.y, depth, 1.0) + world_position = ~view_matrix @ Vec4(view_position.x, view_position.y, z, 1.0) return Vec3(world_position.x, world_position.y, world_position.z) diff --git a/arcade/camera/static.py b/arcade/camera/static.py index 84a100c2cf..716322d59c 100644 --- a/arcade/camera/static.py +++ b/arcade/camera/static.py @@ -15,7 +15,7 @@ ) from arcade.types import Point -from arcade.types.vector_like import Point2, Point3 +from arcade.types.vector_like import Point3 from arcade.window_commands import get_window from pyglet.math import Mat4, Vec3, Vec2 @@ -29,18 +29,16 @@ class _StaticCamera: def __init__(self, view_matrix: Mat4, projection_matrix: Mat4, viewport: Optional[Tuple[int, int, int, int]] = None, *, - project_method: Optional[Callable[[Vec3, Tuple[int, int, int, int], Mat4, Mat4], Vec2]] = None, - unproject_method: Optional[Callable[[Vec2, - Tuple[int, int, int, int], - Mat4, Mat4, Optional[float]], Vec3]] = None, + project_method: Optional[Callable[[Point, Tuple[int, int, int, int], Mat4, Mat4], Vec2]] = None, + unproject_method: Optional[Callable[[Point, Tuple[int, int, int, int], Mat4, Mat4], Vec3]] = None, window: Optional[Window] = None): self._win: Window = window or get_window() self._viewport: Tuple[int, int, int, int] = viewport or self._win.ctx.viewport self._view = view_matrix self._projection = projection_matrix - self._project_method: Optional[Callable[[Vec3, Tuple, Mat4, Mat4], Vec2]] = project_method - self._unproject_method: Optional[Callable[[Vec2, Tuple, Mat4, Mat4, Optional[float]], Vec3]] = unproject_method + self._project_method: Optional[Callable[[Point, Tuple, Mat4, Mat4], Vec2]] = project_method + self._unproject_method: Optional[Callable[[Point, Tuple, Mat4, Mat4], Vec3]] = unproject_method def use(self): self._win.current_camera = self @@ -66,14 +64,12 @@ def project(self, world_coordinate: Point) -> Vec2: raise ValueError("This Static Camera was not provided a project method at creation") pos = self._project_method( - Vec3(world_coordinate[0], world_coordinate[1], world_coordinate[2]), + world_coordinate, self._viewport, self._view, self._projection ) return pos - def unproject(self, - screen_coordinate: Point2, - depth: Optional[float] = None) -> Vec3: + def unproject(self, screen_coordinate: Point) -> Vec3: """ Take in a pixel coordinate from within the range of the window size and returns @@ -84,18 +80,16 @@ def unproject(self, Args: screen_coordinate: A 2D position in pixels from the bottom left of the screen. This should ALWAYS be in the range of 0.0 - screen size. - depth: The depth of the query Returns: A 3D vector in world space. """ if self._unproject_method is None: raise ValueError("This Static Camera was not provided an unproject method at creation") - pos = self._unproject_method( - Vec2(screen_coordinate[0], screen_coordinate[1]), - self._viewport, self._view, self._projection, depth + return self._unproject_method( + screen_coordinate, + self._viewport, self._view, self._projection ) - return pos def static_from_orthographic( @@ -180,7 +174,7 @@ def static_from_matrices( *, window: Optional[Window] = None, project_method: Optional[Callable[[Vec3, Tuple[int, int, int, int], Mat4, Mat4], Vec2]] = None, - unproject_method: Optional[Callable[[Vec2, Tuple[int, int, int, int], Mat4, Mat4, Optional[float]], Vec3]] = None + unproject_method: Optional[Callable[[Vec2, Tuple[int, int, int, int], Mat4, Mat4], Vec3]] = None ) -> _StaticCamera: return _StaticCamera(view, projection, viewport, window=window, project_method=project_method, unproject_method=unproject_method) diff --git a/arcade/draw_commands.py b/arcade/draw_commands.py index d0937bc187..d5864c7835 100644 --- a/arcade/draw_commands.py +++ b/arcade/draw_commands.py @@ -19,7 +19,7 @@ import pyglet.gl as gl from arcade.color import WHITE -from arcade.types import AsFloat, Color, RGBA255, PointList, Point +from arcade.types import AsFloat, Color, RGBA255, PointList, Point, Point2List, Point2 from arcade.earclip import earclip from arcade.types.rect import Rect, LBWH, LRBT, XYWH from .math import rotate_point @@ -567,7 +567,7 @@ def draw_points(point_list: PointList, color: RGBA255, size: float = 1): # --- BEGIN POLYGON FUNCTIONS # # # -def draw_polygon_filled(point_list: PointList, +def draw_polygon_filled(point_list: Point2List, color: RGBA255): """ Draw a polygon that is filled in. @@ -581,7 +581,7 @@ def draw_polygon_filled(point_list: PointList, _generic_draw_line_strip(flattened_list, color, gl.GL_TRIANGLES) -def draw_polygon_outline(point_list: PointList, +def draw_polygon_outline(point_list: Point2List, color: RGBA255, line_width: float = 1): """ Draw a polygon outline. Also known as a "line loop." @@ -612,7 +612,9 @@ def draw_polygon_outline(point_list: PointList, # Use first two points of new list to close the loop new_start, new_next = new_point_list[:2] - points = get_points_for_thick_line(*new_start, *new_next, line_width) + s_x, s_y = new_start + n_x, n_y = new_next + points = get_points_for_thick_line(s_x, s_y, n_x, n_y, line_width) triangle_point_list.append(points[1]) _generic_draw_line_strip(triangle_point_list, color, gl.GL_TRIANGLE_STRIP) diff --git a/arcade/drawing_support.py b/arcade/drawing_support.py index 62d4b3d519..1c745df65b 100644 --- a/arcade/drawing_support.py +++ b/arcade/drawing_support.py @@ -7,13 +7,15 @@ import math from typing import Tuple +from arcade.types import Point2 + __all__ = ["get_points_for_thick_line"] def get_points_for_thick_line(start_x: float, start_y: float, end_x: float, end_y: float, - line_width: float) -> Tuple[Tuple[float, float], Tuple[float, float], - Tuple[float, float], Tuple[float, float]]: + line_width: float) -> Tuple[Point2, Point2, + Point2, Point2]: """ Function used internally for Arcade. OpenGL draws triangles only, so a thick line must be two triangles that make up a rectangle. This calculates and returns diff --git a/arcade/gui/experimental/scroll_area.py b/arcade/gui/experimental/scroll_area.py index f48b168ffb..830d6f0a21 100644 --- a/arcade/gui/experimental/scroll_area.py +++ b/arcade/gui/experimental/scroll_area.py @@ -15,6 +15,7 @@ UIMouseScrollEvent, UIMouseEvent, ) +from arcade.types import LBWH class UIScrollArea(UIWidget): @@ -90,7 +91,7 @@ def do_render(self, surface: Surface): # draw the whole surface, the scissor box, will limit the visible area on screen width, height = self.surface.size self.surface.position = (-self.scroll_x, -self.scroll_y) - self.surface.draw((0, 0, width, height)) + self.surface.draw(LBWH(0, 0, width, height)) def on_event(self, event: UIEvent) -> Optional[bool]: if isinstance(event, UIMouseDragEvent) and not self.rect.collide_with_point(event.x, event.y): diff --git a/arcade/gui/surface.py b/arcade/gui/surface.py index a5d41ccb6a..be93023e8b 100644 --- a/arcade/gui/surface.py +++ b/arcade/gui/surface.py @@ -192,7 +192,7 @@ def draw( self.texture.use(0) self._program["pos"] = self._pos self._program["size"] = self._size - self._program["area"] = area.lbwh or (0, 0, *self._size) + self._program["area"] = (0, 0, *self._size) if not area else area.lbwh self._geometry.render(self._program, vertices=1) # Restore blend function diff --git a/arcade/hitbox/base.py b/arcade/hitbox/base.py index cd43a68038..8b3c31a464 100644 --- a/arcade/hitbox/base.py +++ b/arcade/hitbox/base.py @@ -8,7 +8,7 @@ from PIL.Image import Image -from arcade.types import Point, PointList, EMPTY_POINT_LIST +from arcade.types import Point, Point2, PointList, EMPTY_POINT_LIST __all__ = ["HitBoxAlgorithm", "HitBox", "RotatableHitBox"] @@ -103,7 +103,7 @@ class HitBox: def __init__( self, points: PointList, - position: Point = (0.0, 0.0), + position: Point2 = (0.0, 0.0), scale: Tuple[float, float] = (1.0, 1.0), ): self._points = points diff --git a/arcade/math.py b/arcade/math.py index 8e9fa1b911..db71c6cb36 100644 --- a/arcade/math.py +++ b/arcade/math.py @@ -3,7 +3,7 @@ import math import random from typing import Sequence, Tuple, Union -from arcade.types import AsFloat, Point +from arcade.types import AsFloat, Point, Point2 _PRECISION = 2 @@ -109,7 +109,7 @@ def lerp_angle(start_angle: float, end_angle: float, u: float) -> float: return lerp(start_angle, end_angle, u) % 360 -def rand_in_rect(bottom_left: Point, width: float, height: float) -> Point: +def rand_in_rect(bottom_left: Point2, width: float, height: float) -> Point: """ Calculate a random point in a rectangle. @@ -124,7 +124,7 @@ def rand_in_rect(bottom_left: Point, width: float, height: float) -> Point: ) -def rand_in_circle(center: Point, radius: float) -> Point: +def rand_in_circle(center: Point2, radius: float) -> Point2: """ Generate a point in a circle, or can think of it as a vector pointing a random direction with a random magnitude <= radius. @@ -149,7 +149,7 @@ def rand_in_circle(center: Point, radius: float) -> Point: ) -def rand_on_circle(center: Point, radius: float) -> Point: +def rand_on_circle(center: Point2, radius: float) -> Point2: """ Generate a point on a circle. @@ -167,7 +167,7 @@ def rand_on_circle(center: Point, radius: float) -> Point: ) -def rand_on_line(pos1: Point, pos2: Point) -> Point: +def rand_on_line(pos1: Point2, pos2: Point2) -> Point: """ Given two points defining a line, return a random point on that line. @@ -293,7 +293,7 @@ def rotated(self, angle: float): (self.y * cosine) + (self.x * sine) ) - def as_tuple(self) -> Point: + def as_tuple(self) -> Point2: return self.x, self.y @@ -316,7 +316,7 @@ def rotate_point( cx: float, cy: float, angle_degrees: float, -) -> Point: +) -> Point2: """ Rotate a point around a center. diff --git a/arcade/paths.py b/arcade/paths.py index 5ce1af0f11..6dc109f4d7 100644 --- a/arcade/paths.py +++ b/arcade/paths.py @@ -20,7 +20,7 @@ get_sprites_at_point ) from arcade.math import get_distance, lerp_2d -from arcade.types import Point +from arcade.types import Point, Point2 __all__ = [ "AStarBarrierList", @@ -29,7 +29,7 @@ ] -def _spot_is_blocked(position: Point, +def _spot_is_blocked(position: Point2, moving_sprite: Sprite, blocking_sprites: SpriteList) -> bool: """ @@ -140,7 +140,7 @@ def move_cost(self, a: Point, b: Point) -> float: return 1.42 -def _AStarSearch(start: Point, end: Point, graph: _AStarGraph) -> Optional[List[Point]]: +def _AStarSearch(start: Point2, end: Point2, graph: _AStarGraph) -> Optional[List[Point2]]: """ Returns a path from start to end using the AStarSearch Algorithm @@ -151,15 +151,15 @@ def _AStarSearch(start: Point, end: Point, graph: _AStarGraph) -> Optional[List[ :return: The path from start to end. Returns None if is path is not found """ - G: Dict[Point, float] = {} # Actual movement cost to each position from the start position - F: Dict[Point, float] = {} # Estimated movement cost of start to end going via this position + G: Dict[Point2, float] = dict() # Actual movement cost to each position from the start position + F: Dict[Point2, float] = dict() # Estimated movement cost of start to end going via this position # Initialize starting values G[start] = 0 F[start] = _heuristic(start, end) closed_vertices = set() - open_vertices = {start} + open_vertices = {start} # type: ignore came_from = {} # type: ignore count = 0 diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index f8a7c01733..9f46f615be 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -3,11 +3,10 @@ from typing import TYPE_CHECKING, Iterable, List, TypeVar, Any, Tuple import arcade -from arcade.types import Point, Color, RGBA255, RGBOrA255, PointList +from arcade.types import Point, Point2, Color, RGBA255, RGBOrA255, PointList, Rect, LRBT from arcade.color import BLACK, WHITE from arcade.hitbox import HitBox from arcade.texture import Texture -from arcade.types.rect import Rect, LRBT from arcade.utils import copy_dunders_unimplemented if TYPE_CHECKING: @@ -76,7 +75,7 @@ def __init__( # --- Core Properties --- @property - def position(self) -> Point: + def position(self) -> Point2: """ Get or set the center x and y position of the sprite. @@ -86,7 +85,7 @@ def position(self) -> Point: return self._position @position.setter - def position(self, new_value: Point): + def position(self, new_value: Point2): if new_value == self._position: return @@ -213,12 +212,12 @@ def scale(self, new_value: float): sprite_list._update_size(self) @property - def scale_xy(self) -> Point: + def scale_xy(self) -> Point2: """Get or set the x & y scale of the sprite as a pair of values.""" return self._scale @scale_xy.setter - def scale_xy(self, new_value: Point): + def scale_xy(self, new_value: Point2): if new_value[0] == self._scale[0] and new_value[1] == self._scale[1]: return @@ -651,7 +650,7 @@ def kill(self) -> None: """ self.remove_from_sprite_lists() - def collides_with_point(self, point: Point) -> bool: + def collides_with_point(self, point: Point2) -> bool: """ Check if point is within the current sprite. diff --git a/arcade/sprite/sprite.py b/arcade/sprite/sprite.py index 944d365d7f..cc06b66622 100644 --- a/arcade/sprite/sprite.py +++ b/arcade/sprite/sprite.py @@ -7,7 +7,7 @@ from arcade import Texture, load_texture from arcade.hitbox import HitBox, RotatableHitBox from arcade.texture import get_default_texture -from arcade.types import PathOrTexture, Point +from arcade.types import PathOrTexture, Point2 from arcade.gl.types import OpenGlFilter, BlendFunction from .base import BasicSprite @@ -160,7 +160,7 @@ def radians(self, new_value: float): self.angle = new_value * 180.0 / math.pi @property - def velocity(self) -> Point: + def velocity(self) -> Point2: """ Get or set the velocity of the sprite. @@ -177,7 +177,7 @@ def velocity(self) -> Point: return self._velocity @velocity.setter - def velocity(self, new_value: Point): + def velocity(self, new_value: Point2): self._velocity = new_value @property diff --git a/arcade/text.py b/arcade/text.py index 900ee45a71..17bc63242b 100644 --- a/arcade/text.py +++ b/arcade/text.py @@ -572,12 +572,14 @@ def position(self) -> Point: return self._label.x, self._label.y @position.setter - def position(self, point: Union[Point, Point3]): + def position(self, point: Point): # Starting with Pyglet 2.0b2 label positions take a z parameter. - if len(point) == 3: - self._label.position = point + x, y, *z = Point + + if z: + self._label.position = x, y, z[0] else: - self._label.position = *point, self._label.z + self._label.position = x, y, self._label.z def create_text_sprite( diff --git a/arcade/texture/loading.py b/arcade/texture/loading.py index 9c4c69fe3d..7be1180696 100644 --- a/arcade/texture/loading.py +++ b/arcade/texture/loading.py @@ -178,7 +178,7 @@ def load_texture_pair( def load_textures( file_name: Union[str, Path], - image_location_list: List[Tuple[AsFloat, AsFloat, AsFloat, AsFloat]], + image_location_list: List[Tuple[int, int, int, int]], mirrored: bool = False, flipped: bool = False, hit_box_algorithm: Optional[HitBoxAlgorithm] = None, diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 58b71b6c90..7e507a15c7 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -26,7 +26,6 @@ import sys from pathlib import Path from typing import ( - List, NamedTuple, Optional, Sequence, @@ -86,6 +85,7 @@ from arcade.types.rect import Rect from arcade.types.rect import LRBT +from arcade.types.rect import LBWH from arcade.types.rect import XYWH from arcade.types.rect import XYRR from arcade.types.rect import Viewport @@ -102,10 +102,13 @@ "Point2", "Point3", "PointList", + "Point2List", + "Point3List", "EMPTY_POINT_LIST", "AnchorPoint", "Rect", "LRBT", + "LBWH", "XYWH", "XYRR", "Viewport", @@ -155,6 +158,8 @@ Velocity = Tuple[AsFloat, AsFloat] PointList = Sequence[Point] +Point2List = Sequence[Point2] +Point3List = Sequence[Point3] # Speed / typing workaround: # 1. Eliminate extra allocations # 2. Allows type annotation to be cleaner, primarily for HitBox & subclasses diff --git a/arcade/types/rect.py b/arcade/types/rect.py index f136ceb873..bcd547f8fe 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -477,7 +477,10 @@ def LBWH(left: AsFloat, bottom: AsFloat, width: AsFloat, height: AsFloat) -> Rec def XYWH(x: AsFloat, y: AsFloat, width: AsFloat, height: AsFloat, anchor: Vec2 = AnchorPoint.CENTER) -> Rect: - """Creates a new :py:class:`.Rect` from x, y, width, and height parameters, anchored at a relative point (default center).""" + """ + Creates a new :py:class:`.Rect` from x, y, width, and height parameters, + anchored at a relative point (default center). + """ left = x - anchor.x * width right = left + width bottom = y - anchor.y * height diff --git a/tests/unit/camera/test_orthographic_projector.py b/tests/unit/camera/test_orthographic_projector.py index 7292590954..3c2220d7f2 100644 --- a/tests/unit/camera/test_orthographic_projector.py +++ b/tests/unit/camera/test_orthographic_projector.py @@ -1,5 +1,7 @@ import pytest as pytest +from pyglet.math import Vec3 + from arcade import camera, Window @@ -55,9 +57,9 @@ def test_orthographic_projector_map_coordinates(window: Window, width, height): mouse_pos_c = (230.0, 800.0) # Then - assert ortho_camera.unproject(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert ortho_camera.unproject(mouse_pos_b) == pytest.approx((100.0, 0.0, 0.0)) - assert ortho_camera.unproject(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) + assert tuple(ortho_camera.unproject(mouse_pos_a)) == pytest.approx((100.0, 100.0, 0.0)) + assert tuple(ortho_camera.unproject(mouse_pos_b)) == pytest.approx((100.0, 0.0, 0.0)) + assert tuple(ortho_camera.unproject(mouse_pos_c)) == pytest.approx((230.0, 800.0, 0.0)) @pytest.mark.parametrize("width, height", [(800, 600), (1280, 720), (500, 500)]) @@ -76,9 +78,9 @@ def test_orthographic_projector_map_coordinates_move(window: Window, width, heig default_view.position = (0.0, 0.0, 0.0) # Then - assert ortho_camera.unproject(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) + assert tuple(ortho_camera.unproject(mouse_pos_a)) == pytest.approx((0.0, 0.0, 0.0)) assert ( - ortho_camera.unproject(mouse_pos_b) + tuple(ortho_camera.unproject(mouse_pos_b)) == pytest.approx((-half_width+100.0, -half_height+100, 0.0)) ) @@ -89,9 +91,9 @@ def test_orthographic_projector_map_coordinates_move(window: Window, width, heig default_view.position = (100.0, 100.0, 0.0) # Then - assert ortho_camera.unproject(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert tuple(ortho_camera.unproject(mouse_pos_a)) == pytest.approx((100.0, 100.0, 0.0)) assert ( - ortho_camera.unproject(mouse_pos_b) + tuple(ortho_camera.unproject(mouse_pos_b)) == pytest.approx((-half_width+200.0, -half_height+200.0, 0.0)) ) @@ -114,9 +116,9 @@ def test_orthographic_projector_map_coordinates_rotate(window: Window, width, he default_view.position = (0.0, 0.0, 0.0) # Then - assert ortho_camera.unproject(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) + assert tuple(ortho_camera.unproject(mouse_pos_a)) == pytest.approx((0.0, 0.0, 0.0)) assert ( - ortho_camera.unproject(mouse_pos_b) + tuple(ortho_camera.unproject(mouse_pos_b)) == pytest.approx((-half_height+100.0, half_width-100.0, 0.0)) ) @@ -132,9 +134,9 @@ def test_orthographic_projector_map_coordinates_rotate(window: Window, width, he b_rotated_x = b_shift_x / (2.0**0.5) + b_shift_y / (2.0**0.5) + 100 b_rotated_y = -b_shift_x / (2.0**0.5) + b_shift_y / (2.0**0.5) + 100 # Then - assert ortho_camera.unproject(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert tuple(ortho_camera.unproject(mouse_pos_a)) == pytest.approx((100.0, 100.0, 0.0)) assert ( - ortho_camera.unproject(mouse_pos_b) + tuple(ortho_camera.unproject(mouse_pos_b)) == pytest.approx((b_rotated_x, b_rotated_y, 0.0)) ) @@ -157,12 +159,12 @@ def test_orthographic_projector_map_coordinates_zoom(window: Window, width, heig # Then assert ( - ortho_camera.unproject(mouse_pos_a) + tuple(ortho_camera.unproject(mouse_pos_a)) == - pytest.approx((window.width*0.75, window.height*0.75, 0.0)) + pytest.approx(Vec3(window.width*0.75, window.height*0.75, 0.0)) ) assert ( - ortho_camera.unproject(mouse_pos_b) + tuple(ortho_camera.unproject(mouse_pos_b)) == pytest.approx((half_width + (100 - half_width)*0.5, half_height + (100 - half_height)*0.5, 0.0)) ) @@ -175,12 +177,12 @@ def test_orthographic_projector_map_coordinates_zoom(window: Window, width, heig # Then assert ( - ortho_camera.unproject(mouse_pos_a) + tuple(ortho_camera.unproject(mouse_pos_a)) == pytest.approx((window.width*2.0, window.height*2.0, 0.0)) ) assert ( - ortho_camera.unproject(mouse_pos_b) + tuple(ortho_camera.unproject(mouse_pos_b)) == pytest.approx(((100 - half_width)*4.0, (100 - half_height)*4.0, 0.0)) ) diff --git a/tests/unit/camera/test_perspective_projector.py b/tests/unit/camera/test_perspective_projector.py index 9a078070b8..f3adc19817 100644 --- a/tests/unit/camera/test_perspective_projector.py +++ b/tests/unit/camera/test_perspective_projector.py @@ -59,9 +59,9 @@ def test_perspective_projector_map_coordinates(window: Window, width, height): mouse_pos_c = (230.0, 800.0) # Then - assert persp_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, depth)) - assert persp_camera.map_screen_to_world_coordinate(mouse_pos_b) == pytest.approx((100.0, 0.0, depth)) - assert persp_camera.map_screen_to_world_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, depth)) + assert tuple(persp_camera.unproject(mouse_pos_a)) == pytest.approx((100.0, 100.0, depth)) + assert tuple(persp_camera.unproject(mouse_pos_b)) == pytest.approx((100.0, 0.0, depth)) + assert tuple(persp_camera.unproject(mouse_pos_c)) == pytest.approx((230.0, 800.0, depth)) @pytest.mark.parametrize("width, height", [(800, 600), (1280, 720), (500, 500)]) @@ -82,9 +82,9 @@ def test_perspective_projector_map_coordinates_move(window: Window, width, heigh default_view.position = (0.0, 0.0, 0.0) # Then - assert persp_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, depth)) + assert tuple(persp_camera.unproject(mouse_pos_a)) == pytest.approx((0.0, 0.0, depth)) assert ( - persp_camera.map_screen_to_world_coordinate(mouse_pos_b) + tuple(persp_camera.unproject(mouse_pos_b)) == pytest.approx((-half_width+100.0, -half_height+100, depth)) ) @@ -95,9 +95,9 @@ def test_perspective_projector_map_coordinates_move(window: Window, width, heigh default_view.position = (100.0, 100.0, 0.0) # Then - assert persp_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, depth)) + assert tuple(persp_camera.unproject(mouse_pos_a)) == pytest.approx((100.0, 100.0, depth)) assert ( - persp_camera.map_screen_to_world_coordinate(mouse_pos_b) + tuple(persp_camera.unproject(mouse_pos_b)) == pytest.approx((-half_width+200.0, -half_height+200.0, depth)) ) @@ -122,9 +122,9 @@ def test_perspective_projector_map_coordinates_rotate(window: Window, width, hei default_view.position = (0.0, 0.0, 0.0) # Then - assert persp_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, depth)) + assert tuple(persp_camera.unproject(mouse_pos_a)) == pytest.approx((0.0, 0.0, depth)) assert ( - persp_camera.map_screen_to_world_coordinate(mouse_pos_b) + tuple(persp_camera.unproject(mouse_pos_b)) == pytest.approx((-half_height+100.0, half_width-100.0, depth)) ) @@ -140,9 +140,9 @@ def test_perspective_projector_map_coordinates_rotate(window: Window, width, hei b_rotated_x = b_shift_x / (2.0**0.5) + b_shift_y / (2.0**0.5) + 100 b_rotated_y = -b_shift_x / (2.0**0.5) + b_shift_y / (2.0**0.5) + 100 # Then - assert persp_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, depth)) + assert tuple(persp_camera.unproject(mouse_pos_a)) == pytest.approx((100.0, 100.0, depth)) assert ( - persp_camera.map_screen_to_world_coordinate(mouse_pos_b) + tuple(persp_camera.unproject(mouse_pos_b)) == pytest.approx((b_rotated_x, b_rotated_y, depth)) ) diff --git a/tests/unit/gui/test_layouting_boxlayout.py b/tests/unit/gui/test_layouting_boxlayout.py index 03992fe6af..60a86ad1cf 100644 --- a/tests/unit/gui/test_layouting_boxlayout.py +++ b/tests/unit/gui/test_layouting_boxlayout.py @@ -1,7 +1,7 @@ from _pytest.python_api import approx from arcade.gui import UIBoxLayout, UIManager -from arcade.gui.widgets import UIDummy, Rect +from arcade.gui.widgets import UIDummy, GUIRect # Vertical @@ -11,7 +11,7 @@ def test_do_layout_vertical_with_initial_children(window): element_2 = UIDummy() group = UIBoxLayout(vertical=True, children=[element_1, element_2]) - group.rect = Rect(100, 200, *group.size_hint_min) + group.rect = GUIRect(100, 200, *group.size_hint_min) group._do_layout() @@ -33,7 +33,7 @@ def test_do_layout_vertical_add_children(window): group.add(element_1) group.add(element_2) - group.rect = Rect(100, 200, *group.size_hint_min) + group.rect = GUIRect(100, 200, *group.size_hint_min) group.do_layout() assert element_1.top == 400 @@ -54,7 +54,7 @@ def test_do_layout_vertical_add_child_with_initial_children(window): group.add(element_3) - group.rect = Rect(100, 200, *group.size_hint_min) + group.rect = GUIRect(100, 200, *group.size_hint_min) group.do_layout() assert element_1.top == 500 @@ -76,7 +76,7 @@ def test_do_layout_vertical_align_left(window): group = UIBoxLayout(align="left", vertical=True, children=[element_1, element_2]) - group.rect = Rect(100, 200, *group.size_hint_min) + group.rect = GUIRect(100, 200, *group.size_hint_min) group.do_layout() assert group.left == 100 @@ -94,7 +94,7 @@ def test_do_layout_vertical_align_right(window): group = UIBoxLayout(align="right", vertical=True, children=[element_1, element_2]) - group.rect = Rect(100, 200, *group.size_hint_min) + group.rect = GUIRect(100, 200, *group.size_hint_min) group.do_layout() assert group.left == 100 @@ -112,7 +112,7 @@ def test_do_layout_vertical_space_between(window): group = UIBoxLayout(space_between=10, vertical=True, children=[element_1, element_2]) - group.rect = Rect(100, 200, *group.size_hint_min) + group.rect = GUIRect(100, 200, *group.size_hint_min) group.do_layout() assert group.left == 100 @@ -132,7 +132,7 @@ def test_do_layout_horizontal_with_initial_children(window): group = UIBoxLayout(vertical=False, children=[element_1, element_2]) - group.rect = Rect(100, 200, *group.size_hint_min) + group.rect = GUIRect(100, 200, *group.size_hint_min) group.do_layout() assert element_1.left == 100 @@ -153,7 +153,7 @@ def test_do_layout_horizontal_add_children(window): group.add(element_1) group.add(element_2) - group.rect = Rect(100, 200, *group.size_hint_min) + group.rect = GUIRect(100, 200, *group.size_hint_min) group.do_layout() assert element_1.left == 100 @@ -173,7 +173,7 @@ def test_do_layout_horizontal_add_child_with_initial_children(window): group = UIBoxLayout(vertical=False, children=[element_1, element_2]) group.add(element_3) - group.rect = Rect(100, 200, *group.size_hint_min) + group.rect = GUIRect(100, 200, *group.size_hint_min) group.do_layout() assert element_1.left == 100 @@ -197,7 +197,7 @@ def test_horizontal_group_keep_left_alignment_while_adding_children(window): group = UIBoxLayout(vertical=False, children=[element_1, element_2]) group.add(element_3) - group.rect = Rect(100, 200, *group.size_hint_min) + group.rect = GUIRect(100, 200, *group.size_hint_min) group.do_layout() assert group.left == 100 @@ -211,7 +211,7 @@ def test_do_layout_horizontal_align_top(window): element_2 = UIDummy(height=100) group = UIBoxLayout(align="top", vertical=False, children=[element_1, element_2]) - group.rect = Rect(100, 200, *group.size_hint_min) + group.rect = GUIRect(100, 200, *group.size_hint_min) group.do_layout() assert group.left == 100 @@ -228,7 +228,7 @@ def test_do_layout_horizontal_align_bottom(window): element_2 = UIDummy(height=100) group = UIBoxLayout(align="bottom", vertical=False, children=[element_1, element_2]) - group.rect = Rect(100, 200, *group.size_hint_min) + group.rect = GUIRect(100, 200, *group.size_hint_min) group.do_layout() assert group.left == 100 @@ -245,7 +245,7 @@ def test_do_layout_horizontal_space_between(window): element_2 = UIDummy() group = UIBoxLayout(space_between=10, vertical=False, children=[element_1, element_2]) - group.rect = Rect(100, 200, *group.size_hint_min) + group.rect = GUIRect(100, 200, *group.size_hint_min) group.do_layout() assert group.left == 100 diff --git a/tests/unit/gui/test_layouting_examples.py b/tests/unit/gui/test_layouting_examples.py index 27a805edb4..4e39552be9 100644 --- a/tests/unit/gui/test_layouting_examples.py +++ b/tests/unit/gui/test_layouting_examples.py @@ -1,4 +1,4 @@ -from arcade.gui import UIBoxLayout, UIDummy, Rect +from arcade.gui import UIBoxLayout, UIDummy, GUIRect def test_uiboxlayout_bars_with_size_hint(window): @@ -16,14 +16,14 @@ def test_uiboxlayout_bars_with_size_hint(window): bottom_bar = UIDummy(height=100, size_hint=(1, 0), size_hint_min=(None, 100)) box.add(bottom_bar) - box.rect = Rect(0, 0, 800, 600) + box.rect = GUIRect(0, 0, 800, 600) box._do_layout() box._do_layout() assert box.size == (800, 600) - assert top_bar.rect == Rect(0, 550, 800, 50) - assert center_area.rect == Rect(0, 100, 800, 450) - assert bottom_bar.rect == Rect(0, 0, 800, 100) + assert top_bar.rect == GUIRect(0, 550, 800, 50) + assert center_area.rect == GUIRect(0, 100, 800, 450) + assert bottom_bar.rect == GUIRect(0, 0, 800, 100) def test_uiboxlayout_vertical_bars_with_size_hint(window): @@ -41,11 +41,11 @@ def test_uiboxlayout_vertical_bars_with_size_hint(window): right_bar = UIDummy(size_hint=(0, 1), size_hint_min=(100, None)) box.add(right_bar) - box.rect = Rect(0, 0, 800, 600) + box.rect = GUIRect(0, 0, 800, 600) box._do_layout() # box._do_layout() assert box.size == (800, 600) - assert left_bar.rect == Rect(0, 0, 50, 600) - assert center_area.rect == Rect(50, 0, 650, 600) - assert right_bar.rect == Rect(700, 0, 100, 600) + assert left_bar.rect == GUIRect(0, 0, 50, 600) + assert center_area.rect == GUIRect(50, 0, 650, 600) + assert right_bar.rect == GUIRect(700, 0, 100, 600) diff --git a/tests/unit/gui/test_layouting_gridlayout.py b/tests/unit/gui/test_layouting_gridlayout.py index 324657b142..f50dd17333 100644 --- a/tests/unit/gui/test_layouting_gridlayout.py +++ b/tests/unit/gui/test_layouting_gridlayout.py @@ -1,5 +1,5 @@ from arcade.gui import UIDummy, UIManager, UIBoxLayout, UIAnchorLayout -from arcade.gui.widgets import Rect +from arcade.gui.widgets import GUIRect from arcade.gui.widgets.layout import UIGridLayout @@ -16,7 +16,7 @@ def test_place_widget(window): subject.add(dummy3, 1, 0) subject.add(dummy4, 1, 1) - subject.rect = Rect(0, 0, *subject.size_hint_min) + subject.rect = GUIRect(0, 0, *subject.size_hint_min) subject.do_layout() # check that do_layout doesn't manipulate the rect @@ -35,7 +35,7 @@ def test_can_handle_empty_cells(window): subject.add(dummy1, 0, 0) - subject.rect = Rect(0, 0, *subject.size_hint_min) + subject.rect = GUIRect(0, 0, *subject.size_hint_min) subject.do_layout() # check that do_layout doesn't manipulate the rect @@ -57,7 +57,7 @@ def test_place_widget_with_different_sizes(window): subject.add(dummy3, 1, 0) subject.add(dummy4, 1, 1) - subject.rect = Rect(0, 0, *subject.size_hint_min) + subject.rect = GUIRect(0, 0, *subject.size_hint_min) subject.do_layout() assert subject.rect == (0, 0, 200, 200) @@ -77,7 +77,7 @@ def test_place_widget_within_content_rect(window): assert subject.size_hint_min == (110, 120) - subject.rect = Rect(0, 0, *subject.size_hint_min) + subject.rect = GUIRect(0, 0, *subject.size_hint_min) subject.do_layout() assert dummy1.position == (10, 20) @@ -103,7 +103,7 @@ def test_place_widgets_with_col_row_span(window): subject.add(dummy5, 0, 2, col_span=2) subject.add(dummy6, 2, 0, row_span=3) - subject.rect = Rect(0, 0, *subject.size_hint_min) + subject.rect = GUIRect(0, 0, *subject.size_hint_min) subject.do_layout() assert dummy1.position == (0, 200) @@ -133,7 +133,7 @@ def test_place_widgets_with_col_row_span_and_spacing(window): subject.add(dummy4, 1, 1) subject.add(dummy5, 0, 2, col_span=2) - subject.rect = Rect(0, 0, *subject.size_hint_min) + subject.rect = GUIRect(0, 0, *subject.size_hint_min) subject.do_layout() assert dummy1.position == (10, 200) @@ -168,7 +168,7 @@ def test_adjust_children_size_relative(window): subject.add(dummy3, 1, 0) subject.add(dummy4, 1, 1) - subject.rect = Rect(0, 0, *subject.size_hint_min) + subject.rect = GUIRect(0, 0, *subject.size_hint_min) subject.do_layout() # check that do_layout doesn't manipulate the rect @@ -196,7 +196,7 @@ def test_does_not_adjust_children_without_size_hint(window): subject.add(dummy3, 1, 0) subject.add(dummy4, 1, 1) - subject.rect = Rect(0, 0, *subject.size_hint_min) + subject.rect = GUIRect(0, 0, *subject.size_hint_min) subject.do_layout() # check that do_layout doesn't manipulate the rect @@ -220,7 +220,7 @@ def test_size_hint_and_spacing(window): subject.add(dummy1, 0, 0) - subject.rect = Rect(0, 0, *subject.size_hint_min) + subject.rect = GUIRect(0, 0, *subject.size_hint_min) subject.do_layout() assert dummy1.size == (100, 100) @@ -239,7 +239,7 @@ def test_empty_cells(window): subject.add(dummy1, 2, 2) - subject.rect = Rect(0, 0, *subject.size_hint_min) + subject.rect = GUIRect(0, 0, *subject.size_hint_min) subject.do_layout() assert dummy1.position == (0, 0) diff --git a/tests/unit/gui/test_rect.py b/tests/unit/gui/test_rect.py index ee7268c831..629b2723cf 100644 --- a/tests/unit/gui/test_rect.py +++ b/tests/unit/gui/test_rect.py @@ -1,11 +1,11 @@ from math import ceil -from arcade.gui.widgets import Rect +from arcade.gui.widgets import GUIRect def test_rect_properties(): # GIVEN - rect = Rect(10, 20, 100, 200) + rect = GUIRect(10, 20, 100, 200) # THEN assert rect.x == 10 @@ -20,7 +20,7 @@ def test_rect_properties(): def test_rect_move(): # GIVEN - rect = Rect(10, 20, 100, 200) + rect = GUIRect(10, 20, 100, 200) # WHEN new_rect = rect.move(30, 50) @@ -31,7 +31,7 @@ def test_rect_move(): def test_rect_resize(): # GIVEN - rect = Rect(10, 20, 100, 200) + rect = GUIRect(10, 20, 100, 200) # WHEN new_rect = rect.resize(200, 300) @@ -42,7 +42,7 @@ def test_rect_resize(): def test_rect_align_center_x(): # GIVEN - rect = Rect(10, 20, 100, 200) + rect = GUIRect(10, 20, 100, 200) # WHEN new_rect = rect.align_center_x(50) @@ -53,7 +53,7 @@ def test_rect_align_center_x(): def test_rect_align_center_y(): # GIVEN - rect = Rect(10, 20, 100, 200) + rect = GUIRect(10, 20, 100, 200) # WHEN new_rect = rect.align_center_y(50) @@ -64,7 +64,7 @@ def test_rect_align_center_y(): def test_rect_center(): # WHEN - rect = Rect(0, 0, 100, 200) + rect = GUIRect(0, 0, 100, 200) # THEN assert rect.center == (50, 100) @@ -72,7 +72,7 @@ def test_rect_center(): def test_rect_align_top(): # GIVEN - rect = Rect(10, 20, 100, 200) + rect = GUIRect(10, 20, 100, 200) # WHEN new_rect = rect.align_top(50) @@ -83,7 +83,7 @@ def test_rect_align_top(): def test_rect_align_bottom(): # GIVEN - rect = Rect(10, 20, 100, 200) + rect = GUIRect(10, 20, 100, 200) # WHEN new_rect = rect.align_bottom(50) @@ -94,7 +94,7 @@ def test_rect_align_bottom(): def test_rect_align_right(): # GIVEN - rect = Rect(10, 20, 100, 200) + rect = GUIRect(10, 20, 100, 200) # WHEN new_rect = rect.align_right(50) @@ -105,7 +105,7 @@ def test_rect_align_right(): def test_rect_align_left(): # GIVEN - rect = Rect(10, 20, 100, 200) + rect = GUIRect(10, 20, 100, 200) # WHEN new_rect = rect.align_left(50) @@ -116,7 +116,7 @@ def test_rect_align_left(): def test_rect_min_size(): # GIVEN - rect = Rect(10, 20, 100, 200) + rect = GUIRect(10, 20, 100, 200) # WHEN new_rect = rect.min_size(120, 180) @@ -127,7 +127,7 @@ def test_rect_min_size(): def test_rect_max_size(): # GIVEN - rect = Rect(10, 20, 100, 200) + rect = GUIRect(10, 20, 100, 200) # WHEN new_rect = rect.max_size(120, 180) @@ -138,7 +138,7 @@ def test_rect_max_size(): def test_rect_max_size_only_width(): # GIVEN - rect = Rect(10, 20, 100, 200) + rect = GUIRect(10, 20, 100, 200) # WHEN new_rect = rect.max_size(width=80) @@ -149,7 +149,7 @@ def test_rect_max_size_only_width(): def test_rect_max_size_only_height(): # GIVEN - rect = Rect(10, 20, 100, 200) + rect = GUIRect(10, 20, 100, 200) # WHEN new_rect = rect.max_size(height=80) @@ -160,8 +160,8 @@ def test_rect_max_size_only_height(): def test_rect_union(): # GIVEN - rect_a = Rect(0, 5, 10, 5) - rect_b = Rect(5, 0, 15, 8) + rect_a = GUIRect(0, 5, 10, 5) + rect_b = GUIRect(5, 0, 15, 8) # WHEN new_rect = rect_a.union(rect_b) @@ -171,7 +171,7 @@ def test_rect_union(): def test_collide_with_point(): - rect = Rect(0, 0, 100, 100) + rect = GUIRect(0, 0, 100, 100) assert rect.collide_with_point(0, 0) assert rect.collide_with_point(50, 50) @@ -180,7 +180,7 @@ def test_collide_with_point(): def test_rect_scale(): - rect = Rect(0, 0, 95, 99) + rect = GUIRect(0, 0, 95, 99) # Default rounding rounds down assert rect.scale(0.9) == (0, 0, 85, 89) @@ -189,7 +189,7 @@ def test_rect_scale(): assert rect.scale(0.9, rounding=ceil) == (0, 0, 86, 90) # Passing in None applies no rounding - rect_100 = Rect(100, 100, 100, 100) + rect_100 = GUIRect(100, 100, 100, 100) rect_100_scaled = rect_100.scale(0.1234, None) assert rect_100_scaled == (12.34, 12.34, 12.34, 12.34) assert rect_100_scaled.x == 12.34 diff --git a/tests/unit/gui/test_uilabel.py b/tests/unit/gui/test_uilabel.py index bf57adccfb..8c05faf0fd 100644 --- a/tests/unit/gui/test_uilabel.py +++ b/tests/unit/gui/test_uilabel.py @@ -2,7 +2,7 @@ import pytest -from arcade.gui import UILabel, Rect +from arcade.gui import UILabel, GUIRect from arcade.types import Color @@ -15,17 +15,17 @@ def test_uilabel_inits_with_text_size(window): def test_uilabel_uses_size_parameter(window): label = UILabel(text="Example", width=100, height=50) - assert label.rect == Rect(0, 0, 100, 50) + assert label.rect == GUIRect(0, 0, 100, 50) def test_uilabel_uses_smaller_size_parameter(window): label = UILabel(text="Example", width=20, height=50) - assert label.rect == Rect(0, 0, 20, 50) + assert label.rect == GUIRect(0, 0, 20, 50) def test_uilabel_allow_multiline_and_uses_text_height(window): label = UILabel(text="E x a m p l e", width=10, multiline=True) - assert label.rect == Rect(0, 0, 10, pytest.approx(133, abs=8)) + assert label.rect == GUIRect(0, 0, 10, pytest.approx(133, abs=8)) def test_uilabel_with_border_keeps_previous_size(window): diff --git a/tests/unit/sprite/test_sprite_collision.py b/tests/unit/sprite/test_sprite_collision.py index cffd33b841..942c0ebbeb 100644 --- a/tests/unit/sprite/test_sprite_collision.py +++ b/tests/unit/sprite/test_sprite_collision.py @@ -303,9 +303,9 @@ def test_get_sprites_in_rect(use_spatial_hash): sp.extend((a, b, c, d)) with pytest.raises(TypeError): - arcade.get_sprites_in_rect((0, 0, 10, 10), "moo") + arcade.get_sprites_in_rect(arcade.LRBT(0, 0, 10, 10), "moo") - assert set(arcade.get_sprites_in_rect((-50, 50, -50, 50), sp)) == set([a, b, c, d]) - assert set(arcade.get_sprites_in_rect((100, 200, 100, 200), sp)) == set() - assert set(arcade.get_sprites_in_rect((-100, 0, -100, 0), sp)) == set([b, d]) - assert set(arcade.get_sprites_in_rect((100, 0, 100, 0), sp)) == set([a, c]) + assert set(arcade.get_sprites_in_rect(arcade.LRBT(-50, 50, -50, 50), sp)) == {a, b, c, d} + assert set(arcade.get_sprites_in_rect(arcade.LRBT(100, 200, 100, 200), sp)) == set() + assert set(arcade.get_sprites_in_rect(arcade.LRBT(-100, 0, -100, 0), sp)) == {b, d} + assert set(arcade.get_sprites_in_rect(arcade.LRBT(100, 0, 100, 0), sp)) == {a, c} From 7284c73fb19b38e0bfe8fb567d7d68a21373607d Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 29 May 2024 05:39:10 -0400 Subject: [PATCH 19/52] Added Section.rect --- PR.md | 1 + arcade/geometry.py | 1 - arcade/sections.py | 5 +++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/PR.md b/PR.md index 0c46f661f4..7872db12bd 100644 --- a/PR.md +++ b/PR.md @@ -12,5 +12,6 @@ - Added `Rect` and it's constructors, `Vec2`, and `Vec3` to top-level module - Added `Texture.draw_rect()` - Added `BasicSprite.rect` +- Added `Section.rect` - Remove `IntRect`, `FloatRect`, `RectList` - Rename the old `Rect` to `GUIRect` diff --git a/arcade/geometry.py b/arcade/geometry.py index 778fe20f83..256982bb59 100644 --- a/arcade/geometry.py +++ b/arcade/geometry.py @@ -76,7 +76,6 @@ def is_point_in_box(p: Point, q: Point, r: Point) -> bool: ) -# NOTE: Should be named are_point_in_box def get_triangle_orientation(p: Point, q: Point, r: Point) -> int: """ Find the orientation of a triangle defined by (p, q, r) diff --git a/arcade/sections.py b/arcade/sections.py index 02e8a0c679..5f960e19f2 100644 --- a/arcade/sections.py +++ b/arcade/sections.py @@ -7,6 +7,7 @@ from arcade import get_window from arcade.camera.default import DefaultProjector +from arcade.types.rect import LRBT, Rect if TYPE_CHECKING: from arcade.camera import Projector @@ -231,6 +232,10 @@ def top(self, value: int): self._ec_top = self.window.height if self._modal else value self._ec_bottom = 0 if self._modal else self._bottom + @property + def rect(self) -> Rect: + return LRBT(self.left, self.right, self.bottom, self.top) + @property def window(self): """ The view window """ From cab8a31ddd3d7f658c33e3f5e72588602fdb1d34 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 29 May 2024 05:45:40 -0400 Subject: [PATCH 20/52] add SpriteSolidColor.from_rect --- PR.md | 1 + arcade/sprite/colored.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/PR.md b/PR.md index 7872db12bd..5135d6e714 100644 --- a/PR.md +++ b/PR.md @@ -13,5 +13,6 @@ - Added `Texture.draw_rect()` - Added `BasicSprite.rect` - Added `Section.rect` +- Added `SpriteSolidColor.from_rect()` - Remove `IntRect`, `FloatRect`, `RectList` - Rename the old `Rect` to `GUIRect` diff --git a/arcade/sprite/colored.py b/arcade/sprite/colored.py index c52b7d38d3..235428e7c3 100644 --- a/arcade/sprite/colored.py +++ b/arcade/sprite/colored.py @@ -11,6 +11,7 @@ make_soft_circle_texture, ) from arcade.types import Color, RGBA255 +from arcade.types.rect import Rect from .sprite import Sprite @@ -68,6 +69,10 @@ def __init__( ) self.color = Color.from_iterable(color) + def from_rect(self, rect: Rect, color: Color, angle: float = 0.0) -> SpriteSolidColor: + """Construct a new SpriteSolidColor from a :py:class:`~arcade.types.rect.Rect`.""" + return SpriteSolidColor(int(rect.width), int(rect.height), rect.x, rect.y, color, angle) + class SpriteCircle(Sprite): """ From 2a543affd789967d83374ab7ce5d2c2214c3a659 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 29 May 2024 06:23:31 -0400 Subject: [PATCH 21/52] from_rect --- arcade/gui/nine_patch.py | 8 +++++++- arcade/sprite/colored.py | 5 +++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/arcade/gui/nine_patch.py b/arcade/gui/nine_patch.py index 1a5e152c9c..20cc50e7b8 100644 --- a/arcade/gui/nine_patch.py +++ b/arcade/gui/nine_patch.py @@ -4,6 +4,7 @@ import arcade import arcade.gl as gl +from arcade.types.rect import Rect class NinePatchTexture: @@ -69,12 +70,12 @@ class NinePatchTexture: def __init__( self, - *, left: int, right: int, bottom: int, top: int, texture: arcade.Texture, + *, atlas: Optional[arcade.TextureAtlas] = None, ): self._ctx = arcade.get_window().ctx @@ -105,6 +106,11 @@ def __init__( self._check_sizes() + @classmethod + def from_rect(cls, rect: Rect, texture: arcade.Texture, atlas: Optional[arcade.TextureAtlas] = None) -> NinePatchTexture: + """Construct a new SpriteSolidColor from a :py:class:`~arcade.types.rect.Rect`.""" + return cls(*rect.viewport, texture, atlas=atlas) + @property def ctx(self) -> arcade.ArcadeContext: """The OpenGL context.""" diff --git a/arcade/sprite/colored.py b/arcade/sprite/colored.py index 235428e7c3..021438da48 100644 --- a/arcade/sprite/colored.py +++ b/arcade/sprite/colored.py @@ -69,9 +69,10 @@ def __init__( ) self.color = Color.from_iterable(color) - def from_rect(self, rect: Rect, color: Color, angle: float = 0.0) -> SpriteSolidColor: + @classmethod + def from_rect(cls, rect: Rect, color: Color, angle: float = 0.0) -> SpriteSolidColor: """Construct a new SpriteSolidColor from a :py:class:`~arcade.types.rect.Rect`.""" - return SpriteSolidColor(int(rect.width), int(rect.height), rect.x, rect.y, color, angle) + return cls(int(rect.width), int(rect.height), rect.x, rect.y, color, angle) class SpriteCircle(Sprite): From 62f4a28937b3d1c0bdcdfeed729ad0f7f49a6a40 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 29 May 2024 06:23:39 -0400 Subject: [PATCH 22/52] __mul__ and __div__ --- PR.md | 2 ++ arcade/types/rect.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/PR.md b/PR.md index 5135d6e714..151c099437 100644 --- a/PR.md +++ b/PR.md @@ -4,6 +4,7 @@ - `Rect` - Added `Rect.distance_from_bounds()` - Added `point in rect` support for `Rect` + - Added `*` and `/` support for scaling relative to `(0, 0)`. - Functions expecting `Vec2` now accept `Tuple[AsFloat, AsFloat]` - Improved docstrings - Added type aliases `Point2` and `Point3` @@ -14,5 +15,6 @@ - Added `BasicSprite.rect` - Added `Section.rect` - Added `SpriteSolidColor.from_rect()` +- Added `NinePatchTexture.from_rect()` - Remove `IntRect`, `FloatRect`, `RectList` - Rename the old `Rect` to `GUIRect` diff --git a/arcade/types/rect.py b/arcade/types/rect.py index bcd547f8fe..603108ad9d 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -195,6 +195,16 @@ def scale_axes(self, new_scale: Point2, anchor: Vec2 = AnchorPoint.CENTER) -> Re return LRBT(adjusted_left, adjusted_right, adjusted_bottom, adjusted_top) + def __mul__(self, scale: AsFloat) -> Rect: + """Scale the Rect by ``scale`` relative to ``(0, 0)``.""" + return Rect(self.left * scale, self.right * scale, self.bottom * scale, self.top * scale, + self.width * scale, self.height * scale, self.x * scale, self.y * scale) + + def __div__(self, scale: AsFloat) -> Rect: + """Scale the Rect by 1/``scale`` relative to ``(0, 0)``.""" + return Rect(self.left / scale, self.right / scale, self.bottom / scale, self.top / scale, + self.width / scale, self.height / scale, self.x / scale, self.y / scale) + def align_top(self, value: AsFloat) -> Rect: """Returns a new :py:class:`~arcade.types.rect.Rect`, which is aligned to the top at `value`.""" return LBWH(self.left, value - self.height, self.width, self.height) From f95961302dd7687ff51223b6fe14ef7fc6cc6b5e Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 29 May 2024 06:38:46 -0400 Subject: [PATCH 23/52] new doublescore methods --- PR.md | 4 ++++ arcade/gui/nine_patch.py | 2 +- arcade/types/rect.py | 41 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/PR.md b/PR.md index 151c099437..9586aba9a5 100644 --- a/PR.md +++ b/PR.md @@ -5,8 +5,12 @@ - Added `Rect.distance_from_bounds()` - Added `point in rect` support for `Rect` - Added `*` and `/` support for scaling relative to `(0, 0)`. + - Added bool() support (area is not 0) + - Added support for round(), floor(), and ceil() + - Added `.area` property - Functions expecting `Vec2` now accept `Tuple[AsFloat, AsFloat]` - Improved docstrings + - Fixed `.viewport` - Added type aliases `Point2` and `Point3` - Camera - All camera functions now take `Point`, `Point2`, or `Point3` where points are expected diff --git a/arcade/gui/nine_patch.py b/arcade/gui/nine_patch.py index 20cc50e7b8..f8a0ff8add 100644 --- a/arcade/gui/nine_patch.py +++ b/arcade/gui/nine_patch.py @@ -109,7 +109,7 @@ def __init__( @classmethod def from_rect(cls, rect: Rect, texture: arcade.Texture, atlas: Optional[arcade.TextureAtlas] = None) -> NinePatchTexture: """Construct a new SpriteSolidColor from a :py:class:`~arcade.types.rect.Rect`.""" - return cls(*rect.viewport, texture, atlas=atlas) + return cls(int(rect.left), int(rect.right), int(rect.bottom), int(rect.top), texture, atlas=atlas) @property def ctx(self) -> arcade.ArcadeContext: diff --git a/arcade/types/rect.py b/arcade/types/rect.py index 603108ad9d..5155b139f1 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -1,5 +1,6 @@ """Rects all act the same, but take four of the possible eight attributes and calculate the rest.""" from __future__ import annotations +import math from typing import NamedTuple, Optional, TypedDict, Tuple from pyglet.math import Vec2 @@ -128,6 +129,11 @@ def size(self) -> Vec2: """Returns a :py:class:`~pyglet.math.Vec2` representing the size of the rectangle.""" return Vec2(self.width, self.height) + @property + def area(self) -> float: + """The area of the rectangle in square pixels.""" + return self.width * self.height + @property def aspect_ratio(self) -> float: """Returns the ratio between the width and the height.""" @@ -415,8 +421,8 @@ def xyrr(self) -> RectParams: @property def viewport(self) -> ViewportParams: - """Provides a tuple in the format of (left, right, bottom, top), coerced to integers.""" - return (int(self.left), int(self.right), int(self.bottom), int(self.top)) + """Provides a tuple in the format of (left, right, width, height), coerced to integers.""" + return (int(self.left), int(self.right), int(self.width), int(self.height)) @classmethod def from_kwargs(cls, **kwargs: AsFloat) -> Rect: @@ -465,6 +471,37 @@ def __str__(self) -> str: f"<{self.__class__.__name__} LRBT({self.left}, {self.right}, {self.bottom}, {self.top})" f" XYWH({self.x}, {self.y}, {self.width}, {self.height})>") + def __bool__(self) -> bool: + """Returns True if area is not 0, else False.""" + return self.width != 0 or self.height != 0 + + def __round__(self, n: int) -> Rect: + """Rounds the left, right, bottom, and top to `n` decimals.""" + return LRBT( + round(self.left, n), + round(self.right, n), + round(self.bottom, n), + round(self.top, n) + ) + + def __floor__(self) -> Rect: + """Floors the left, right, bottom, and top.""" + return LRBT( + math.floor(self.left), + math.floor(self.right), + math.floor(self.bottom), + math.floor(self.top) + ) + + def __ceil__(self) -> Rect: + """Floors the left, right, bottom, and top.""" + return LRBT( + math.ceil(self.left), + math.ceil(self.right), + math.ceil(self.bottom), + math.ceil(self.top) + ) + # Shorthand creation helpers From cc895679718c7177bb8e0c52b4e8addc4b09ca33 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Wed, 29 May 2024 23:57:37 +1200 Subject: [PATCH 24/52] Rectifying camera and correcting all issues this causes I am very tired. Almost certain I missed some --- arcade/camera/camera_2d.py | 168 +++++++++--------- arcade/camera/data_types.py | 128 ++++++++++--- arcade/camera/orthographic.py | 20 ++- arcade/camera/perspective.py | 23 ++- arcade/camera/static.py | 2 +- arcade/examples/camera_platform.py | 5 +- arcade/examples/minimap.py | 5 +- arcade/examples/minimap_camera.py | 15 +- arcade/examples/perspective.py | 4 +- arcade/gui/surface.py | 10 +- arcade/gui/ui_manager.py | 10 +- arcade/text.py | 2 +- arcade/texture_atlas/atlas_2d.py | 7 +- arcade/types/rect.py | 13 +- tests/unit/camera/test_camera2d.py | 11 +- .../unit/camera/test_perspective_projector.py | 6 +- tests/unit/gui/test_uimanager_camera.py | 6 +- tests/unit/rect/test_rect_instances.py | 2 +- 18 files changed, 265 insertions(+), 172 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 1cbbc95861..ac1f8069ee 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -13,7 +13,7 @@ from arcade.gl import Framebuffer from pyglet.math import Vec2, Vec3 -from arcade.types import Point +from arcade.types import Point, Rect, LRBT, LBWH from arcade.types.vector_like import Point2 from arcade.window_commands import get_window @@ -68,14 +68,15 @@ class Camera2D: """ def __init__(self, - viewport: Optional[Tuple[int, int, int, int]] = None, - position: Optional[Tuple[float, float]] = None, + viewport: Optional[Rect] = None, + position: Optional[Point2] = None, up: Tuple[float, float] = (0.0, 1.0), zoom: float = 1.0, - projection: Optional[Tuple[float, float, float, float]] = None, + projection: Optional[Rect] = None, near: float = -100.0, far: float = 100.0, *, + scissor: Optional[Rect] = None, render_target: Optional[Framebuffer] = None, window: Optional["Window"] = None): @@ -83,16 +84,21 @@ def __init__(self, self.render_target: Optional[Framebuffer] = render_target # We don't want to force people to use a render target, - # but we need to have some form of default size so we use the screen. + # but we need to have some form of default size. render_target = render_target or self._window.ctx.screen - viewport = render_target.viewport if viewport is None else viewport - width, height = (render_target.width, render_target.height) if viewport is None else (viewport[2], viewport[3]) + viewport = viewport or LBWH(*render_target.viewport) + width, height = viewport.size half_width = width / 2 half_height = height / 2 # Unpack projection, but only validate when it's given directly - left, right, bottom, top = projection or (-half_width, half_width, -half_height, half_height) - if projection: + left, right, bottom, top = ( + (-half_width, half_width, -half_height, half_height) + if projection is None else + projection.lrbt + ) + + if projection is not None: if left == right: raise ZeroProjectionDimension(( f"projection width is 0 due to equal {left=}" @@ -117,13 +123,14 @@ def __init__(self, self._projection_data: OrthographicProjectionData = OrthographicProjectionData( left=left, right=right, top=top, bottom=bottom, - near=near, far=far, - viewport=(viewport[0], viewport[1], viewport[2], viewport[3]) + near=near, far=far ) self._ortho_projector: OrthographicProjector = OrthographicProjector( window=self._window, view=self._camera_data, - projection=self._projection_data + projection=self._projection_data, + viewport=viewport, + scissor=scissor ) @classmethod @@ -131,6 +138,8 @@ def from_camera_data(cls, *, camera_data: Optional[CameraData] = None, projection_data: Optional[OrthographicProjectionData] = None, render_target: Optional[Framebuffer] = None, + viewport: Optional[Rect] = None, + scissor: Optional[Rect] = None, window: Optional["Window"] = None) -> Self: """ Make a ``Camera2D`` directly from data objects. @@ -188,7 +197,7 @@ def from_camera_data(cls, *, ) # build a new camera with defaults and then apply the provided camera objects. - new_camera = cls(render_target=render_target, window=window) + new_camera = cls(render_target=render_target, window=window, viewport=viewport, scissor=scissor) if camera_data: new_camera._camera_data = camera_data if projection_data: @@ -197,7 +206,9 @@ def from_camera_data(cls, *, new_camera._ortho_projector = OrthographicProjector( window=new_camera._window, view=new_camera._camera_data, - projection=new_camera._projection_data + projection=new_camera._projection_data, + viewport=new_camera.viewport, + scissor=new_camera.scissor ) return new_camera @@ -242,7 +253,6 @@ def position(self) -> Vec2: @position.setter def position(self, _pos: Point2) -> None: - x, y = _pos self._camera_data.position = (x, y, self._camera_data.position[2]) @@ -432,7 +442,7 @@ def point_in_view(self, point: Point2) -> bool: return abs(dot_x) <= h_width and abs(dot_y) <= h_height @property - def projection(self) -> Tuple[float, float, float, float]: + def projection(self) -> Rect: """Get/set the left, right, bottom, and top projection values. These are world space values which control how the camera @@ -448,32 +458,22 @@ def projection(self) -> Tuple[float, float, float, float]: exception if any axis pairs are equal. You can handle this exception as a :py:class:`ValueError`. """ - _p = self._projection_data - _z = self._camera_data.zoom - return _p.left / _z, _p.right / _z, _p.bottom / _z, _p.top / _z + + return self._projection_data.rect / self._camera_data.zoom @projection.setter - def projection(self, value: Tuple[float, float, float, float]) -> None: + def projection(self, value: Rect) -> None: # Unpack and validate - left, right, bottom, top = value - if left == right: + if not value: raise ZeroProjectionDimension(( - f"projection width is 0 due to equal {left=}" - f"and {right=} values")) - if bottom == top: - raise ZeroProjectionDimension(( - f"projection height is 0 due to equal {bottom=}" - f"and {top=}")) + f"Projection area is 0, {value.lrbt}" + )) _z = self._camera_data.zoom # Modify the projection data itself. - _p = self._projection_data - _p.left = left * _z - _p.right = right * _z - _p.bottom = bottom * _z - _p.top = top * _z + self._projection_data.rect = value * _z @property def width(self) -> float: @@ -488,13 +488,13 @@ def width(self) -> float: return (self._projection_data.right - self._projection_data.left) / self._camera_data.zoom @width.setter - def width(self, _width: float) -> None: + def width(self, new_width: float) -> None: w = self.width l = self.left / w # Normalised Projection left r = self.right / w # Normalised Projection Right - self.left = l * _width - self.right = r * _width + self.left = l * new_width + self.right = r * new_width @property def height(self) -> float: @@ -509,13 +509,13 @@ def height(self) -> float: return (self._projection_data.top - self._projection_data.bottom) / self._camera_data.zoom @height.setter - def height(self, _height: float) -> None: - h = self.projection_height + def height(self, new_height: float) -> None: + h = self.height b = self.bottom / h # Normalised Projection Bottom t = self.top / h # Normalised Projection Top - self.bottom = b * _height - self.top = t * _height + self.bottom = b * new_height + self.top = t * new_height @property def left(self) -> float: @@ -530,8 +530,8 @@ def left(self) -> float: return self._projection_data.left / self._camera_data.zoom @left.setter - def left(self, _left: float) -> None: - self._projection_data.left = _left * self._camera_data.zoom + def left(self, new_left: float) -> None: + self._projection_data.left = new_left * self._camera_data.zoom @property def right(self) -> float: @@ -546,8 +546,8 @@ def right(self) -> float: return self._projection_data.right / self._camera_data.zoom @right.setter - def right(self, _right: float) -> None: - self._projection_data.right = _right * self._camera_data.zoom + def right(self, new_right: float) -> None: + self._projection_data.right = new_right * self._camera_data.zoom @property def bottom(self) -> float: @@ -562,8 +562,8 @@ def bottom(self) -> float: return self._projection_data.bottom / self._camera_data.zoom @bottom.setter - def bottom(self, _bottom: float) -> None: - self._projection_data.bottom = _bottom * self._camera_data.zoom + def bottom(self, new_bottom: float) -> None: + self._projection_data.bottom = new_bottom * self._camera_data.zoom @property def top(self) -> float: @@ -578,8 +578,8 @@ def top(self) -> float: return self._projection_data.top / self._camera_data.zoom @top.setter - def top(self, _top: float) -> None: - self._projection_data.top = _top * self._camera_data.zoom + def top(self, new_top: float) -> None: + self._projection_data.top = new_top * self._camera_data.zoom @property def projection_near(self) -> float: @@ -592,8 +592,8 @@ def projection_near(self) -> float: return self._projection_data.near @projection_near.setter - def projection_near(self, _near: float) -> None: - self._projection_data.near = _near + def projection_near(self, new_near: float) -> None: + self._projection_data.near = new_near @property def projection_far(self) -> float: @@ -606,22 +606,30 @@ def projection_far(self) -> float: return self._projection_data.far @projection_far.setter - def projection_far(self, _far: float) -> None: - self._projection_data.far = _far + def projection_far(self, new_far: float) -> None: + self._projection_data.far = new_far @property - def viewport(self) -> Tuple[int, int, int, int]: + def viewport(self) -> Rect: """Get/set pixels of the ``render_target`` drawn to when active. The pixel area is defined as integer pixel coordinates starting from the bottom left of ``self.render_target``. They are ordered as ``(left, bottom, width, height)``. """ - return self._projection_data.viewport + return self._ortho_projector.viewport @viewport.setter - def viewport(self, _viewport: Tuple[int, int, int, int]) -> None: - self._projection_data.viewport = _viewport + def viewport(self, viewport: Rect) -> None: + self._ortho_projector.viewport = viewport + + @property + def scissor(self) -> Rect: + return self._ortho_projector.scissor + + @scissor.setter + def scissor(self, scissor: Rect): + self._ortho_projector.scissor = scissor @property def viewport_width(self) -> int: @@ -629,12 +637,11 @@ def viewport_width(self) -> int: The width of the viewport. Defines the number of pixels drawn too horizontally. """ - return self._projection_data.viewport[2] + return int(self._ortho_projector.viewport.width) @viewport_width.setter - def viewport_width(self, _width: int) -> None: - self._projection_data.viewport = (self._projection_data.viewport[0], self._projection_data.viewport[1], - _width, self._projection_data.viewport[3]) + def viewport_width(self, new_width: int) -> None: + self._ortho_projector.viewport.resize(new_width, anchor=Vec2(0.0, 0.0)) @property def viewport_height(self) -> int: @@ -642,70 +649,66 @@ def viewport_height(self) -> int: The height of the viewport. Defines the number of pixels drawn too vertically. """ - return self._projection_data.viewport[3] + return int(self._ortho_projector.viewport.height) @viewport_height.setter - def viewport_height(self, _height: int) -> None: - self._projection_data.viewport = (self._projection_data.viewport[0], self._projection_data.viewport[1], - self._projection_data.viewport[2], _height) + def viewport_height(self, new_height: int) -> None: + self._ortho_projector.viewport.resize(height=new_height, anchor=Vec2(0.0, 0.0)) @property def viewport_left(self) -> int: """ The left most pixel drawn to on the X axis. """ - return self._projection_data.viewport[0] + return int(self._ortho_projector.viewport.left) @viewport_left.setter - def viewport_left(self, _left: int) -> None: - self._projection_data.viewport = (_left,) + self._projection_data.viewport[1:] + def viewport_left(self, new_left: int) -> None: + self._ortho_projector.viewport = self._ortho_projector.viewport.align_left(new_left) @property def viewport_right(self) -> int: """ The right most pixel drawn to on the X axis. """ - return self._projection_data.viewport[0] + self._projection_data.viewport[2] + return int(self._ortho_projector.viewport.right) @viewport_right.setter - def viewport_right(self, _right: int) -> None: + def viewport_right(self, new_right: int) -> None: """ Set the right most pixel drawn to on the X axis. This moves the position of the viewport, not change the size. """ - self._projection_data.viewport = (_right - self._projection_data.viewport[2], self._projection_data.viewport[1], - self._projection_data.viewport[2], self._projection_data.viewport[3]) + self._ortho_projector.viewport = self._ortho_projector.viewport.align_right(new_right) @property def viewport_bottom(self) -> int: """ The bottom most pixel drawn to on the Y axis. """ - return self._projection_data.viewport[1] + return int(self._ortho_projector.viewport.bottom) @viewport_bottom.setter - def viewport_bottom(self, _bottom: int) -> None: + def viewport_bottom(self, new_bottom: int) -> None: """ Set the bottom most pixel drawn to on the Y axis. """ - self._projection_data.viewport = (self._projection_data.viewport[0], _bottom, - self._projection_data.viewport[2], self._projection_data.viewport[3]) + self._ortho_projector.viewport = self._ortho_projector.viewport.align_bottom(new_bottom) @property def viewport_top(self) -> int: """ The top most pixel drawn to on the Y axis. """ - return self._projection_data.viewport[1] + self._projection_data.viewport[3] + return int(self._ortho_projector.viewport.top) @viewport_top.setter - def viewport_top(self, _top: int) -> None: + def viewport_top(self, new_top: int) -> None: """ Set the top most pixel drawn to on the Y axis. This moves the position of the viewport, not change the size. """ - self._projection_data.viewport = (self._projection_data.viewport[0], _top - self._projection_data.viewport[3], - self._projection_data.viewport[2], self._projection_data.viewport[3]) + self._ortho_projector.viewport = self._ortho_projector.viewport.align_top(new_top) @property def up(self) -> Vec2: @@ -729,7 +732,8 @@ def up(self, _up: Point2) -> None: NOTE that this is assumed to be normalised. """ - self._camera_data.up = (_up[0], _up[1], 0.0) + x, y = _up + self._camera_data.up = (x, y, 0.0) @property def angle(self) -> float: @@ -785,8 +789,8 @@ def equalise(self) -> None: the projections center in the same relative place. """ - self.projection_width = self.viewport_width - self.projection_height = self.viewport_height + self.width = self.viewport_width + self.height = self.viewport_height def match_screen(self, and_projection: bool = True) -> None: """ diff --git a/arcade/camera/data_types.py b/arcade/camera/data_types.py index 5af6e1423c..1f77636283 100644 --- a/arcade/camera/data_types.py +++ b/arcade/camera/data_types.py @@ -10,8 +10,7 @@ from typing_extensions import Self from pyglet.math import Vec2, Vec3 -from arcade.types import Point -from arcade.types.vector_like import Point3 +from arcade.types import Point, Point3, Rect, LRBT, AsFloat __all__ = [ @@ -39,7 +38,6 @@ class ZeroProjectionDimension(ValueError): ... - class CameraData: """Stores position, orientation, and zoom for a camera. @@ -118,7 +116,7 @@ class OrthographicProjectionData: viewport: The pixel bounds which will be drawn onto. (left, bottom, width, height) """ - __slots__ = ("left", "right", "bottom", "top", "near", "far", "viewport") + __slots__ = ("rect", "near", "far") def __init__( self, @@ -127,31 +125,119 @@ def __init__( bottom: float, top: float, near: float, - far: float, - viewport: Tuple[int, int, int, int]): + far: float + ): # Data for generating Orthographic Projection matrix - self.left: float = left - self.right: float = right - self.bottom: float = bottom - self.top: float = top + self.rect: Rect = LRBT(left, right, bottom, top) self.near: float = near self.far: float = far - # Viewport for setting which pixels to draw to - self.viewport: Tuple[int, int, int, int] = viewport + @property + def left(self) -> float: + return self.rect.left + + @left.setter + def left(self, new_left: AsFloat): + r = self.rect + dl = new_left - r.left + self.rect = Rect( + new_left, + r.right, + r.bottom, + r.top, + r.width + dl, + r.height, + r.x + dl / 2.0, + r.y + ) + + @property + def right(self) -> float: + return self.rect.right + + @right.setter + def right(self, new_right: AsFloat): + r = self.rect + dr = new_right - r.right + self.rect = Rect( + r.left, + new_right, + r.bottom, + r.top, + r.width + dr, + r.height, + r.x + dr / 2.0, + r.y + ) + + @property + def bottom(self) -> float: + return self.rect.bottom + + @bottom.setter + def bottom(self, new_bottom: AsFloat): + r = self.rect + db = new_bottom - r.bottom + self.rect = Rect( + r.left, + r.right, + new_bottom, + r.top, + r.width, + r.height + db, + r.x, + r.y + db / 2.0 + ) + + @property + def top(self) -> float: + return self.rect.top + + @top.setter + def top(self, new_top: AsFloat): + r = self.rect + dt = new_top - r.top + self.rect = Rect( + r.left, + r.right, + r.bottom, + new_top, + r.width, + r.height + dt, + r.x, + r.y + dt / 2.0 + ) + + @property + def lrbt(self) -> tuple[float, float, float, float]: + return self.rect.lrbt + + @lrbt.setter + def lrbt(self, new_lrbt: tuple[float, float, float, float]): + self.rect = LRBT(*new_lrbt) def __str__(self): return (f"OrthographicProjection<" - f"LRBT={(self.left, self.right, self.bottom, self.top)}, " + f"LRBT={self.rect.lrbt}, " f"{self.near=}, " - f"{self.far=}, " - f"{self.viewport=}>") + f"{self.far=}") def __repr__(self): return self.__str__() +def orthographic_from_rect(rect: Rect, near: float, far: float) -> OrthographicProjectionData: + return OrthographicProjectionData( + rect.left, + rect.right, + rect.bottom, + rect.top, + near, + far + ) + + class PerspectiveProjectionData: """Describes a perspective projection. @@ -163,26 +249,21 @@ class PerspectiveProjectionData: far: The 'furthest' value, Which gets mapped to z = 1.0 (anything above this value is not visible). viewport: The pixel bounds which will be drawn onto. (left, bottom, width, height) """ - __slots__ = ("aspect", "fov", "near", "far", "viewport") + __slots__ = ("aspect", "fov", "near", "far") def __init__(self, aspect: float, fov: float, near: float, - far: float, - - viewport: Tuple[int, int, int, int]): + far: float): # Data for generating Perspective Projection matrix self.aspect: float = aspect self.fov: float = fov self.near: float = near self.far: float = far - # Viewport for setting which pixels to draw to - self.viewport: Tuple[int, int, int, int] = viewport - def __str__(self): - return f"PerspectiveProjection<{self.aspect=}, {self.fov=}, {self.near=}, {self.far=}, {self.viewport=}>" + return f"PerspectiveProjection<{self.aspect=}, {self.fov=}, {self.near=}, {self.far=}>" def __repr__(self): return self.__str__() @@ -228,7 +309,6 @@ class Projection(Protocol): camera's :py:attr:`.CameraData.forward` vector. """ - viewport: Tuple[int, int, int, int] near: float far: float diff --git a/arcade/camera/orthographic.py b/arcade/camera/orthographic.py index 19e2d5e9e7..51e91e65e2 100644 --- a/arcade/camera/orthographic.py +++ b/arcade/camera/orthographic.py @@ -12,7 +12,7 @@ unproject_orthographic ) -from arcade.types import Point +from arcade.types import Point, Rect, LBWH from arcade.window_commands import get_window if TYPE_CHECKING: from arcade import Window @@ -50,9 +50,15 @@ class OrthographicProjector(Projector): def __init__(self, *, window: Optional["Window"] = None, view: Optional[CameraData] = None, - projection: Optional[OrthographicProjectionData] = None): + projection: Optional[OrthographicProjectionData] = None, + viewport: Optional[Rect] = None, + scissor: Optional[Rect] = None + ): self._window: "Window" = window or get_window() + self.viewport: Rect = viewport or LBWH(0, 0, self._window.width, self._window.height) + self.scissor: Optional[Rect] = scissor + self._view = view or CameraData( # Viewport (self._window.width / 2, self._window.height / 2, 0), # Position (0.0, 1.0, 0.0), # Up @@ -64,8 +70,6 @@ def __init__(self, *, -0.5 * self._window.width, 0.5 * self._window.width, # Left, Right -0.5 * self._window.height, 0.5 * self._window.height, # Bottom, Top -100, 100, # Near, Far - - (0, 0, self._window.width, self._window.height) # Viewport ) @property @@ -106,7 +110,8 @@ def use(self) -> None: _projection = generate_orthographic_matrix(self._projection, self._view.zoom) _view = generate_view_matrix(self._view) - self._window.ctx.viewport = self._projection.viewport + self._window.ctx.viewport = self.viewport.viewport + self._window.ctx.scissor = None if not self.scissor else self.scissor.viewport self._window.projection = _projection self._window.view = _view @@ -143,7 +148,7 @@ def project(self, world_coordinate: Point) -> Vec2: _view = generate_view_matrix(self._view) return project_orthographic( - world_coordinate, self.projection.viewport, + world_coordinate, self.viewport.viewport, _view, _projection, ) @@ -165,9 +170,8 @@ def unproject(self, screen_coordinate: Point) -> Vec3: _projection = generate_orthographic_matrix(self._projection, self._view.zoom) _view = generate_view_matrix(self._view) - return unproject_orthographic( screen_coordinate, - self.projection.viewport, + self.viewport.viewport, _view, _projection ) diff --git a/arcade/camera/perspective.py b/arcade/camera/perspective.py index fbcee0715f..e0a4371c06 100644 --- a/arcade/camera/perspective.py +++ b/arcade/camera/perspective.py @@ -13,7 +13,7 @@ unproject_perspective ) -from arcade.types import Point +from arcade.types import Point, Rect, LBWH from arcade.window_commands import get_window if TYPE_CHECKING: from arcade import Window @@ -50,9 +50,14 @@ class PerspectiveProjector(Projector): def __init__(self, *, window: Optional["Window"] = None, view: Optional[CameraData] = None, - projection: Optional[PerspectiveProjectionData] = None): + projection: Optional[PerspectiveProjectionData] = None, + viewport: Optional[Rect] = None, + scissor: Optional[Rect] = None): self._window: "Window" = window or get_window() + self.viewport: Rect = viewport or LBWH(0, 0, self._window.width, self._window.height) + self.scissor: Optional[Rect] = scissor + self._view = view or CameraData( # Viewport (self._window.width / 2, self._window.height / 2, 0), # Position (0.0, 1.0, 0.0), # Up @@ -63,8 +68,7 @@ def __init__(self, *, self._projection = projection or PerspectiveProjectionData( self._window.width / self._window.height, # Aspect 60, # Field of View, - 0.01, 100.0, # near, # far - (0, 0, self._window.width, self._window.height) # Viewport + 0.01, 100.0 # near, # far ) @property @@ -130,7 +134,8 @@ def use(self) -> None: _projection = generate_perspective_matrix(self._projection, self._view.zoom) _view = generate_view_matrix(self._view) - self._window.ctx.viewport = self._projection.viewport + self._window.ctx.viewport = self.viewport.viewport + self._window.ctx.scissor = None if not self.scissor else self.scissor.viewport self._window.projection = _projection self._window.view = _view @@ -139,7 +144,7 @@ def project(self, world_coordinate: Point) -> Vec2: Take a Vec2 or Vec3 of coordinates and return the related screen coordinate """ x, y, *z = world_coordinate - z = (0.5 * self._projection.viewport[3] / tan( + z = (0.5 * self.viewport.height / tan( radians(0.5 * self._projection.fov / self._view.zoom))) if not z else z[0] _projection = generate_perspective_matrix(self._projection, self._view.zoom) @@ -147,7 +152,7 @@ def project(self, world_coordinate: Point) -> Vec2: pos = project_perspective( Vec3(x, y, z), - self._projection.viewport, + self.viewport.viewport, _view, _projection ) @@ -169,14 +174,14 @@ def unproject(self, screen_coordinate: Point) -> Vec3: A 3D vector in world space. """ x, y, *z = screen_coordinate - z = (0.5 * self._projection.viewport[3] / tan( + z = (0.5 * self.viewport.height / tan( radians(0.5 * self._projection.fov / self._view.zoom))) if not z else z[0] _projection = generate_perspective_matrix(self._projection, self._view.zoom) _view = generate_view_matrix(self._view) pos = unproject_perspective( - Vec3(x, y, z), self.projection.viewport, + Vec3(x, y, z), self.viewport.viewport, _view, _projection ) return pos diff --git a/arcade/camera/static.py b/arcade/camera/static.py index 716322d59c..d1f775cb80 100644 --- a/arcade/camera/static.py +++ b/arcade/camera/static.py @@ -138,7 +138,7 @@ def static_from_raw_orthographic( ) proj = generate_orthographic_matrix( OrthographicProjectionData( - projection[0], projection[1], projection[2], projection[3], near, far, viewport or (0, 0, 0, 0)), zoom + projection[0], projection[1], projection[2], projection[3], near, far), zoom ) return _StaticCamera(view, proj, viewport, window=window, project_method=project_orthographic, diff --git a/arcade/examples/camera_platform.py b/arcade/examples/camera_platform.py index ee6ba482e2..cb87777583 100644 --- a/arcade/examples/camera_platform.py +++ b/arcade/examples/camera_platform.py @@ -131,9 +131,8 @@ def setup(self): self.player_sprite.center_y = 128 self.scene.add_sprite("Player", self.player_sprite) - viewport = (0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) - self.camera = arcade.camera.Camera2D(viewport=viewport) - self.gui_camera = arcade.camera.Camera2D(viewport=viewport) + self.camera = arcade.camera.Camera2D() + self.gui_camera = arcade.camera.Camera2D() self.camera_shake = arcade.camera.grips.ScreenShake2D(self.camera.view_data, max_amplitude=12.5, diff --git a/arcade/examples/minimap.py b/arcade/examples/minimap.py index 8b251e8eb6..d19c8ea21c 100644 --- a/arcade/examples/minimap.py +++ b/arcade/examples/minimap.py @@ -61,9 +61,8 @@ def __init__(self, width, height, title): self.physics_engine = None # Camera for sprites, and one for our GUI - viewport = (0, 0, DEFAULT_SCREEN_WIDTH, DEFAULT_SCREEN_HEIGHT) - self.camera_sprites = arcade.camera.Camera2D(viewport=viewport) - self.camera_gui = arcade.camera.Camera2D(viewport=viewport) + self.camera_sprites = arcade.camera.Camera2D() + self.camera_gui = arcade.camera.Camera2D() def setup(self): """ Set up the game and initialize the variables. """ diff --git a/arcade/examples/minimap_camera.py b/arcade/examples/minimap_camera.py index d2d554ecb2..007c1941da 100644 --- a/arcade/examples/minimap_camera.py +++ b/arcade/examples/minimap_camera.py @@ -52,11 +52,11 @@ def __init__(self, width, height, title): self.wall_list = None # Mini-map related - minimap_viewport = (DEFAULT_SCREEN_WIDTH - MINIMAP_WIDTH, - DEFAULT_SCREEN_HEIGHT - MINIMAP_HEIGHT, - MINIMAP_WIDTH, MINIMAP_HEIGHT) - minimap_projection = (-MAP_PROJECTION_WIDTH/2, MAP_PROJECTION_WIDTH/2, - -MAP_PROJECTION_HEIGHT/2, MAP_PROJECTION_HEIGHT/2) + minimap_viewport = arcade.LBWH(DEFAULT_SCREEN_WIDTH - MINIMAP_WIDTH, + DEFAULT_SCREEN_HEIGHT - MINIMAP_HEIGHT, + MINIMAP_WIDTH, MINIMAP_HEIGHT) + minimap_projection = arcade.LRBT(-MAP_PROJECTION_WIDTH/2, MAP_PROJECTION_WIDTH/2, + -MAP_PROJECTION_HEIGHT/2, MAP_PROJECTION_HEIGHT/2) self.camera_minimap = arcade.camera.Camera2D( viewport=minimap_viewport, projection=minimap_projection ) @@ -67,9 +67,8 @@ def __init__(self, width, height, title): self.physics_engine = None # Camera for sprites, and one for our GUI - viewport = (0, 0, DEFAULT_SCREEN_WIDTH, DEFAULT_SCREEN_HEIGHT) - self.camera_sprites = arcade.camera.Camera2D(viewport=viewport) - self.camera_gui = arcade.camera.Camera2D(viewport=viewport) + self.camera_sprites = arcade.camera.Camera2D() + self.camera_gui = arcade.camera.Camera2D() self.selected_camera = self.camera_minimap diff --git a/arcade/examples/perspective.py b/arcade/examples/perspective.py index a2215ac902..52a19c7ffe 100644 --- a/arcade/examples/perspective.py +++ b/arcade/examples/perspective.py @@ -108,8 +108,8 @@ def __init__(self): self.offscreen_cam = arcade.camera.Camera2D( position=(0.0, 0.0), - viewport=(0, 0, self.fbo.width, self.fbo.height), - projection=(0, self.fbo.width, 0, self.fbo.height) + viewport=arcade.LBWH(0, 0, self.fbo.width, self.fbo.height), + projection=arcade.LRBT(0, self.fbo.width, 0, self.fbo.height) ) def on_draw(self): diff --git a/arcade/gui/surface.py b/arcade/gui/surface.py index be93023e8b..b295045030 100644 --- a/arcade/gui/surface.py +++ b/arcade/gui/surface.py @@ -11,7 +11,7 @@ from arcade.gl import Framebuffer from arcade.gui.nine_patch import NinePatchTexture from arcade.types import RGBA255, Point -from arcade.types.rect import Rect +from arcade.types.rect import Rect, LBWH, LRBT class Surface: @@ -57,8 +57,9 @@ def __init__( self._cam = OrthographicProjector( view=CameraData(), projection=OrthographicProjectionData( - 0.0, self.width, 0.0, self.height, -100, 100, (0, 0, self.width, self.height) + 0.0, self.width, 0.0, self.height, -100, 100 ), + viewport=LBWH(0, 0, self.width, self.height) ) @property @@ -167,9 +168,8 @@ def limit(self, x, y, width, height): width = max(width, 1) height = max(height, 1) - _p = self._cam.projection - _p.left, _p.right, _p.bottom, _p.top = 0, width, 0, height - self._cam.projection.viewport = viewport + self._cam.projection.lrbt = 0, width, 0, height + self._cam.viewport = LBWH(*viewport) self._cam.use() diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index 8f2fa7dc9d..5e43f39fac 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -356,23 +356,23 @@ def dispatch_ui_event(self, event): def on_mouse_motion(self, x: int, y: int, dx: int, dy: int): x_, y_ = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMouseMovementEvent(self, int(x_), int(y), dx, dy)) + return self.dispatch_ui_event(UIMouseMovementEvent(self, round(x_), round(y), dx, dy)) def on_mouse_press(self, x: int, y: int, button: int, modifiers: int): x_, y_ = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMousePressEvent(self, int(x_), int(y_), button, modifiers)) + return self.dispatch_ui_event(UIMousePressEvent(self, round(x_), round(y_), button, modifiers)) def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int): x_, y_ = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMouseDragEvent(self, int(x_), int(y_), dx, dy, buttons, modifiers)) + return self.dispatch_ui_event(UIMouseDragEvent(self, round(x_), round(y_), dx, dy, buttons, modifiers)) def on_mouse_release(self, x: int, y: int, button: int, modifiers: int): x_, y_ = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMouseReleaseEvent(self, int(x_), int(y_), button, modifiers)) + return self.dispatch_ui_event(UIMouseReleaseEvent(self, round(x_), round(y_), button, modifiers)) def on_mouse_scroll(self, x, y, scroll_x, scroll_y): x_, y_ = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMouseScrollEvent(self, int(x_), int(y_), scroll_x, scroll_y)) + return self.dispatch_ui_event(UIMouseScrollEvent(self, round(x_), round(y_), scroll_x, scroll_y)) def on_key_press(self, symbol: int, modifiers: int): return self.dispatch_ui_event(UIKeyPressEvent(self, symbol, modifiers)) # type: ignore diff --git a/arcade/text.py b/arcade/text.py index 17bc63242b..e2a47eeaaa 100644 --- a/arcade/text.py +++ b/arcade/text.py @@ -574,7 +574,7 @@ def position(self) -> Point: @position.setter def position(self, point: Point): # Starting with Pyglet 2.0b2 label positions take a z parameter. - x, y, *z = Point + x, y, *z = point if z: self._label.position = x, y, z[0] diff --git a/arcade/texture_atlas/atlas_2d.py b/arcade/texture_atlas/atlas_2d.py index 898032ac4b..16d6ae7a59 100644 --- a/arcade/texture_atlas/atlas_2d.py +++ b/arcade/texture_atlas/atlas_2d.py @@ -17,7 +17,6 @@ from contextlib import contextmanager from weakref import WeakSet, WeakValueDictionary -import PIL import PIL.Image from PIL import Image, ImageDraw from pyglet.image.atlas import ( @@ -961,16 +960,14 @@ def render_into( static_camera = static_from_raw_orthographic( projection, - -1, 1, # near, far planes + -1, 1, # near, far planes 1.0, # zoom - viewport=(region.x, region.y, region.width, region.height) # viewport ) with self._fbo.activate() as fbo: - fbo.viewport = region.x, region.y, region.width, region.height try: static_camera.use() - print(self.ctx.view_matrix) + fbo.viewport = region.x, region.y, region.width, region.height yield fbo finally: fbo.viewport = 0, 0, *self._fbo.size diff --git a/arcade/types/rect.py b/arcade/types/rect.py index 5155b139f1..faa382d800 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -151,11 +151,18 @@ def move(self, dx: AsFloat = 0.0, dy: AsFloat = 0.0) -> Rect: """ return XYWH(self.x + dx, self.y + dy, self.width, self.height) - def resize(self, width: AsFloat, height: AsFloat, anchor: Vec2 = AnchorPoint.CENTER) -> Rect: + def resize(self, + width: Optional[AsFloat] = None, + height: Optional[AsFloat] = None, + anchor: Vec2 = AnchorPoint.CENTER + ) -> Rect: """ Returns a new :py:class:`~arcade.types.rect.Rect` at the current Rect's position, but with a new width and height, anchored at a point (default center.) """ + width = width or self.width + height = height or self.height + anchor_x = self.left + anchor.x * self.width anchor_y = self.bottom + anchor.y * self.height @@ -421,8 +428,8 @@ def xyrr(self) -> RectParams: @property def viewport(self) -> ViewportParams: - """Provides a tuple in the format of (left, right, width, height), coerced to integers.""" - return (int(self.left), int(self.right), int(self.width), int(self.height)) + """Provides a tuple in the format of (left, bottom, width, height), coerced to integers.""" + return (int(self.left), int(self.bottom), int(self.width), int(self.height)) @classmethod def from_kwargs(cls, **kwargs: AsFloat) -> Rect: diff --git a/tests/unit/camera/test_camera2d.py b/tests/unit/camera/test_camera2d.py index 9dd06b361f..d8fcf3070c 100644 --- a/tests/unit/camera/test_camera2d.py +++ b/tests/unit/camera/test_camera2d.py @@ -2,7 +2,7 @@ import pytest as pytest -from arcade import Window +from arcade import Window, LRBT from arcade.camera import Camera2D from arcade.camera.data_types import ZeroProjectionDimension, OrthographicProjectionData @@ -52,8 +52,7 @@ def test_camera2d_from_camera_data_projection_xy_pairs_equal_raises_zeroprojecti camera_class ): data = OrthographicProjectionData( - *bad_projection, -100.0, 100.0, - viewport=(0, 0, 800, 600) + *bad_projection, -100.0, 100.0 ) with pytest.raises(ZeroProjectionDimension): @@ -67,7 +66,7 @@ def test_camera2d_init_xy_pairs_equal_raises_zeroprojectiondimension( ): with pytest.raises(ZeroProjectionDimension): - _ = camera_class(projection=bad_projection) + _ = camera_class(projection=LRBT(*bad_projection)) def test_camera2d_init_equal_near_far_raises_zeroprojectiondimension( @@ -107,7 +106,7 @@ def test_camera2d_init_uses_render_target_size(window: Window, width, height): assert ortho_camera.viewport_width == width assert ortho_camera.viewport_height == height - assert ortho_camera.viewport == (0, 0, width, height) + assert ortho_camera.viewport.viewport == (0, 0, width, height) assert ortho_camera.viewport_left == 0 assert ortho_camera.viewport_right == width assert ortho_camera.viewport_bottom == 0 @@ -125,7 +124,7 @@ def test_camera2d_from_camera_data_uses_render_target_size(window: Window, width assert ortho_camera.viewport_width == width assert ortho_camera.viewport_height == height - assert ortho_camera.viewport == (0, 0, width, height) + assert ortho_camera.viewport.viewport == (0, 0, width, height) assert ortho_camera.viewport_left == 0 assert ortho_camera.viewport_right == width assert ortho_camera.viewport_bottom == 0 diff --git a/tests/unit/camera/test_perspective_projector.py b/tests/unit/camera/test_perspective_projector.py index f3adc19817..8201e6ed3d 100644 --- a/tests/unit/camera/test_perspective_projector.py +++ b/tests/unit/camera/test_perspective_projector.py @@ -51,7 +51,7 @@ def test_perspective_projector_map_coordinates(window: Window, width, height): window.set_size(width, height) persp_camera = camera.PerspectiveProjector() - depth = (0.5 * persp_camera._projection.viewport[3] / tan(radians(0.5 * persp_camera._projection.fov))) + depth = (0.5 * persp_camera.viewport.height / tan(radians(0.5 * persp_camera._projection.fov))) # When mouse_pos_a = (100.0, 100.0) @@ -71,7 +71,7 @@ def test_perspective_projector_map_coordinates_move(window: Window, width, heigh persp_camera = camera.PerspectiveProjector() default_view = persp_camera.view - depth = (0.5 * persp_camera._projection.viewport[3] / tan(radians(0.5 * persp_camera._projection.fov))) + depth = (0.5 * persp_camera.viewport.height / tan(radians(0.5 * persp_camera._projection.fov))) half_width, half_height = window.width//2, window.height//2 @@ -110,7 +110,7 @@ def test_perspective_projector_map_coordinates_rotate(window: Window, width, hei persp_camera = camera.PerspectiveProjector() default_view = persp_camera.view - depth = (0.5 * persp_camera._projection.viewport[3] / tan(radians(0.5 * persp_camera._projection.fov))) + depth = (0.5 * persp_camera.viewport.height / tan(radians(0.5 * persp_camera._projection.fov))) half_width, half_height = window.width//2, window.height//2 diff --git a/tests/unit/gui/test_uimanager_camera.py b/tests/unit/gui/test_uimanager_camera.py index 7862f1e297..d4fdf75cc9 100644 --- a/tests/unit/gui/test_uimanager_camera.py +++ b/tests/unit/gui/test_uimanager_camera.py @@ -10,11 +10,11 @@ def test_ui_manager_respects_camera_viewport(uimanager, window): # GIVEN uimanager.use_super_mouse_adjustment = True camera = arcade.camera.Camera2D( - position=(0.0, 0.0), projection=(0.0, window.width, 0.0, window.height), window=window + position=(0.0, 0.0), projection=arcade.LRBT(0.0, window.width, 0.0, window.height), window=window ) # WHEN - camera.viewport = 0, 0, 400, 200 + camera.viewport = arcade.LBWH(0, 0, 400, 200) camera.use() uimanager.click(100, 100) @@ -29,7 +29,7 @@ def test_ui_manager_respects_camera_pos(uimanager, window): # GIVEN uimanager.use_super_mouse_adjustment = True camera = arcade.camera.Camera2D( - position=(0.0, 0.0), projection=(0.0, window.width, 0.0, window.height), window=window + position=(0.0, 0.0), projection=arcade.LRBT(0.0, window.width, 0.0, window.height), window=window ) # WHEN diff --git a/tests/unit/rect/test_rect_instances.py b/tests/unit/rect/test_rect_instances.py index 4806afe034..129c1cb802 100644 --- a/tests/unit/rect/test_rect_instances.py +++ b/tests/unit/rect/test_rect_instances.py @@ -95,7 +95,7 @@ def test_views(): assert A_RECT.lbwh == (10, 10, 10, 10) assert A_RECT.xyrr == (15, 15, 5, 5) assert A_RECT.xywh == (15, 15, 10, 10) - assert A_RECT.viewport == (10, 20, 10, 20) + assert A_RECT.viewport == (10, 10, 10, 10) class SubclassedRect(Rect): From 092cc095ae96e0a8cc9da9f135055add29bea0ad Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Thu, 30 May 2024 00:14:18 +1200 Subject: [PATCH 25/52] linting touchups + __truediv__ fix for rect --- arcade/camera/camera_2d.py | 6 +++--- arcade/camera/static.py | 11 ++++++----- arcade/draw_commands.py | 2 +- arcade/gui/nine_patch.py | 6 +++++- arcade/gui/surface.py | 2 +- arcade/text.py | 2 +- arcade/texture/loading.py | 1 - arcade/types/rect.py | 2 +- 8 files changed, 18 insertions(+), 14 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index ac1f8069ee..8f455870b4 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -13,7 +13,7 @@ from arcade.gl import Framebuffer from pyglet.math import Vec2, Vec3 -from arcade.types import Point, Rect, LRBT, LBWH +from arcade.types import Point, Rect, LBWH from arcade.types.vector_like import Point2 from arcade.window_commands import get_window @@ -624,7 +624,7 @@ def viewport(self, viewport: Rect) -> None: self._ortho_projector.viewport = viewport @property - def scissor(self) -> Rect: + def scissor(self) -> Optional[Rect]: return self._ortho_projector.scissor @scissor.setter @@ -800,7 +800,7 @@ def match_screen(self, and_projection: bool = True) -> None: Args: and_projection: Flag whether to also equalise the projection to the viewport. """ - self.viewport = (0, 0, self._window.width, self._window.height) + self.viewport = LBWH(0, 0, self._window.width, self._window.height) if and_projection: self.equalise() diff --git a/arcade/camera/static.py b/arcade/camera/static.py index d1f775cb80..bc69f84e98 100644 --- a/arcade/camera/static.py +++ b/arcade/camera/static.py @@ -14,8 +14,7 @@ unproject_perspective ) -from arcade.types import Point -from arcade.types.vector_like import Point3 +from arcade.types import Point, Point3 from arcade.window_commands import get_window from pyglet.math import Mat4, Vec3, Vec2 @@ -95,13 +94,14 @@ def unproject(self, screen_coordinate: Point) -> Vec3: def static_from_orthographic( view: CameraData, orthographic: OrthographicProjectionData, + viewport: Optional[Tuple[int, int, int, int]] = None, *, window: Optional[Window] = None ) -> _StaticCamera: return _StaticCamera( generate_view_matrix(view), generate_orthographic_matrix(orthographic, view.zoom), - orthographic.viewport, window=window, + viewport, window=window, project_method=project_orthographic, unproject_method=unproject_orthographic ) @@ -110,13 +110,14 @@ def static_from_orthographic( def static_from_perspective( view: CameraData, perspective: OrthographicProjectionData, + viewport: Optional[Tuple[int, int, int, int]] = None, *, window: Optional[Window] = None ) -> _StaticCamera: return _StaticCamera( generate_view_matrix(view), generate_orthographic_matrix(perspective, view.zoom), - perspective.viewport, window=window, + viewport, window=window, project_method=project_perspective, unproject_method=unproject_perspective ) @@ -160,7 +161,7 @@ def static_from_raw_perspective( CameraData(position, up, forward, zoom) ) proj = generate_perspective_matrix( - PerspectiveProjectionData(aspect, fov, near, far, viewport or (0, 0, 0, 0)), zoom + PerspectiveProjectionData(aspect, fov, near, far), zoom ) return _StaticCamera(view, proj, viewport, window=window, diff --git a/arcade/draw_commands.py b/arcade/draw_commands.py index d5864c7835..dc197ab18f 100644 --- a/arcade/draw_commands.py +++ b/arcade/draw_commands.py @@ -19,7 +19,7 @@ import pyglet.gl as gl from arcade.color import WHITE -from arcade.types import AsFloat, Color, RGBA255, PointList, Point, Point2List, Point2 +from arcade.types import AsFloat, Color, RGBA255, PointList, Point, Point2List from arcade.earclip import earclip from arcade.types.rect import Rect, LBWH, LRBT, XYWH from .math import rotate_point diff --git a/arcade/gui/nine_patch.py b/arcade/gui/nine_patch.py index f8a0ff8add..e60da684f2 100644 --- a/arcade/gui/nine_patch.py +++ b/arcade/gui/nine_patch.py @@ -107,7 +107,11 @@ def __init__( self._check_sizes() @classmethod - def from_rect(cls, rect: Rect, texture: arcade.Texture, atlas: Optional[arcade.TextureAtlas] = None) -> NinePatchTexture: + def from_rect(cls, + rect: Rect, + texture: arcade.Texture, + atlas: Optional[arcade.TextureAtlas] = None + ) -> NinePatchTexture: """Construct a new SpriteSolidColor from a :py:class:`~arcade.types.rect.Rect`.""" return cls(int(rect.left), int(rect.right), int(rect.bottom), int(rect.top), texture, atlas=atlas) diff --git a/arcade/gui/surface.py b/arcade/gui/surface.py index b295045030..26bb4c52de 100644 --- a/arcade/gui/surface.py +++ b/arcade/gui/surface.py @@ -11,7 +11,7 @@ from arcade.gl import Framebuffer from arcade.gui.nine_patch import NinePatchTexture from arcade.types import RGBA255, Point -from arcade.types.rect import Rect, LBWH, LRBT +from arcade.types.rect import Rect, LBWH class Surface: diff --git a/arcade/text.py b/arcade/text.py index e2a47eeaaa..400bdc44ff 100644 --- a/arcade/text.py +++ b/arcade/text.py @@ -10,7 +10,7 @@ import arcade from arcade.resources import resolve -from arcade.types import Color, Point, RGBA255, Point3, RGBOrA255 +from arcade.types import Color, Point, RGBA255, RGBOrA255 from arcade.utils import PerformanceWarning, warning __all__ = [ diff --git a/arcade/texture/loading.py b/arcade/texture/loading.py index 7be1180696..43fdc8f730 100644 --- a/arcade/texture/loading.py +++ b/arcade/texture/loading.py @@ -12,7 +12,6 @@ from arcade.hitbox import HitBoxAlgorithm from arcade import cache as _cache from arcade import hitbox -from arcade.types.numbers import AsFloat from .texture import Texture, ImageData LOG = logging.getLogger(__name__) diff --git a/arcade/types/rect.py b/arcade/types/rect.py index faa382d800..1a0f19360b 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -213,7 +213,7 @@ def __mul__(self, scale: AsFloat) -> Rect: return Rect(self.left * scale, self.right * scale, self.bottom * scale, self.top * scale, self.width * scale, self.height * scale, self.x * scale, self.y * scale) - def __div__(self, scale: AsFloat) -> Rect: + def __truediv__(self, scale: AsFloat) -> Rect: """Scale the Rect by 1/``scale`` relative to ``(0, 0)``.""" return Rect(self.left / scale, self.right / scale, self.bottom / scale, self.top / scale, self.width / scale, self.height / scale, self.x / scale, self.y / scale) From 630c57bfc523a1fab67eae62e9e5fed42359cdab Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Thu, 30 May 2024 08:52:21 +1200 Subject: [PATCH 26/52] linting continued --- arcade/cache/hit_box.py | 8 ++++---- arcade/hitbox/__init__.py | 6 +++--- arcade/hitbox/base.py | 26 +++++++++++++------------- arcade/hitbox/bounding_box.py | 4 ++-- arcade/hitbox/pymunk.py | 8 ++++---- arcade/hitbox/simple.py | 4 ++-- arcade/texture/texture.py | 10 +++++----- arcade/texture/transforms.py | 34 +++++++++++++++++----------------- arcade/tilemap/tilemap.py | 10 +++++----- arcade/types/__init__.py | 2 +- 10 files changed, 56 insertions(+), 56 deletions(-) diff --git a/arcade/cache/hit_box.py b/arcade/cache/hit_box.py index 01b21af2c9..f02f9f102b 100644 --- a/arcade/cache/hit_box.py +++ b/arcade/cache/hit_box.py @@ -16,7 +16,7 @@ from typing import Optional, Union, TYPE_CHECKING from collections import OrderedDict -from arcade.types import PointList +from arcade.types import Point2List from arcade.resources import resolve if TYPE_CHECKING: @@ -36,7 +36,7 @@ class HitBoxCache: VERSION = 1 def __init__(self): - self._entries: OrderedDict[str, PointList] = OrderedDict() + self._entries: OrderedDict[str, Point2List] = OrderedDict() def __len__(self) -> int: return len(self._entries) @@ -44,7 +44,7 @@ def __len__(self) -> int: def __iter__(self): return iter(self._entries) - def get(self, name_or_texture: Union[str, "Texture"]) -> Optional[PointList]: + def get(self, name_or_texture: Union[str, "Texture"]) -> Optional[Point2List]: """ Get the hit box points for a texture with a given hash and hit box algorithm. @@ -68,7 +68,7 @@ def get(self, name_or_texture: Union[str, "Texture"]) -> Optional[PointList]: else: raise TypeError(f"Expected str or Texture: {name_or_texture}") - def put(self, name_or_texture: Union[str, "Texture"], points: PointList) -> None: + def put(self, name_or_texture: Union[str, "Texture"], points: Point2List) -> None: """ Store hit box points for a texture. diff --git a/arcade/hitbox/__init__.py b/arcade/hitbox/__init__.py index a73e9dbd7c..e67ae13605 100644 --- a/arcade/hitbox/__init__.py +++ b/arcade/hitbox/__init__.py @@ -2,7 +2,7 @@ from PIL.Image import Image -from arcade.types import PointList +from arcade.types import Point2List from .base import HitBox, HitBoxAlgorithm, RotatableHitBox from .bounding_box import BoundingHitBoxAlgorithm @@ -20,7 +20,7 @@ # Temporary functions for backwards compatibility -def calculate_hit_box_points_simple(image: Image, *args) -> PointList: +def calculate_hit_box_points_simple(image: Image, *args) -> Point2List: """ Given an RGBA image, this returns points that make up a hit box around it. Attempts to trim out transparent pixels. @@ -35,7 +35,7 @@ def calculate_hit_box_points_simple(image: Image, *args) -> PointList: def calculate_hit_box_points_detailed( image: Image, hit_box_detail: float = 4.5, -) -> PointList: +) -> Point2List: """ Given an RGBA image, this returns points that make up a hit box around it. Attempts to trim out transparent pixels. diff --git a/arcade/hitbox/base.py b/arcade/hitbox/base.py index 8b3c31a464..80d238b246 100644 --- a/arcade/hitbox/base.py +++ b/arcade/hitbox/base.py @@ -8,7 +8,7 @@ from PIL.Image import Image -from arcade.types import Point, Point2, PointList, EMPTY_POINT_LIST +from arcade.types import Point, Point2, Point2List, EMPTY_POINT_LIST __all__ = ["HitBoxAlgorithm", "HitBox", "RotatableHitBox"] @@ -41,7 +41,7 @@ def cache_name(self) -> str: """ return self._cache_name - def calculate(self, image: Image, **kwargs) -> PointList: + def calculate(self, image: Image, **kwargs) -> Point2List: """ Calculate hit box points for a given image. @@ -67,7 +67,7 @@ def __call__(self, *args: Any, **kwds: Any) -> Self: """ return self.__class__(*args, **kwds) # type: ignore - def create_bounding_box(self, image: Image) -> PointList: + def create_bounding_box(self, image: Image) -> Point2List: """ Create points for a simple bounding box around an image. This is often used as a fallback if a hit box algorithm @@ -102,7 +102,7 @@ class HitBox: """ def __init__( self, - points: PointList, + points: Point2List, position: Point2 = (0.0, 0.0), scale: Tuple[float, float] = (1.0, 1.0), ): @@ -112,11 +112,11 @@ def __init__( # This empty tuple will be replaced the first time # get_adjusted_points is called - self._adjusted_points: PointList = EMPTY_POINT_LIST + self._adjusted_points: Point2List = EMPTY_POINT_LIST self._adjusted_cache_dirty = True @property - def points(self) -> PointList: + def points(self) -> Point2List: """ The raw, unadjusted points of this hit box. @@ -126,7 +126,7 @@ def points(self) -> PointList: return self._points @property - def position(self) -> Point: + def position(self) -> Point2: """ The center point used to offset the final adjusted positions. :return: @@ -134,7 +134,7 @@ def position(self) -> Point: return self._position @position.setter - def position(self, position: Point): + def position(self, position: Point2): self._position = position self._adjusted_cache_dirty = True @@ -210,7 +210,7 @@ def create_rotatable( self._points, position=self._position, scale=self._scale, angle=angle ) - def get_adjusted_points(self) -> Sequence[Point]: + def get_adjusted_points(self) -> Point2List: """ Return the positions of points, scaled and offset from the center. @@ -223,7 +223,7 @@ def get_adjusted_points(self) -> Sequence[Point]: if not self._adjusted_cache_dirty: return self._adjusted_points # type: ignore - def _adjust_point(point) -> Point: + def _adjust_point(point) -> Point2: x, y = point x *= self.scale[0] @@ -245,7 +245,7 @@ class RotatableHitBox(HitBox): """ def __init__( self, - points: PointList, + points: Point2List, *, position: Tuple[float, float] = (0.0, 0.0), angle: float = 0.0, @@ -266,7 +266,7 @@ def angle(self, angle: float): self._angle = angle self._adjusted_cache_dirty = True - def get_adjusted_points(self) -> PointList: + def get_adjusted_points(self) -> Point2List: """ Return the offset, scaled, & rotated points of this hitbox. @@ -281,7 +281,7 @@ def get_adjusted_points(self) -> PointList: rad_cos = cos(rad) rad_sin = sin(rad) - def _adjust_point(point) -> Point: + def _adjust_point(point) -> Point2: x, y = point x *= self.scale[0] diff --git a/arcade/hitbox/bounding_box.py b/arcade/hitbox/bounding_box.py index b0dfee8478..671aba9886 100644 --- a/arcade/hitbox/bounding_box.py +++ b/arcade/hitbox/bounding_box.py @@ -1,7 +1,7 @@ from __future__ import annotations from PIL.Image import Image -from arcade.types import PointList +from arcade.types import Point2List from .base import HitBoxAlgorithm @@ -11,7 +11,7 @@ class BoundingHitBoxAlgorithm(HitBoxAlgorithm): """ cache = False - def calculate(self, image: Image, **kwargs) -> PointList: + def calculate(self, image: Image, **kwargs) -> Point2List: """ Given an RGBA image, this returns points that make up a hit box around it without any attempt to trim out transparent pixels. diff --git a/arcade/hitbox/pymunk.py b/arcade/hitbox/pymunk.py index cca46dbefa..d17ee4bd5e 100644 --- a/arcade/hitbox/pymunk.py +++ b/arcade/hitbox/pymunk.py @@ -9,7 +9,7 @@ simplify_curves, ) from pymunk import Vec2d -from arcade.types import Point, PointList +from arcade.types import Point2, Point2List from .base import HitBoxAlgorithm @@ -33,7 +33,7 @@ def __call__(self, *, detail: Optional[float] = None) -> "PymunkHitBoxAlgorithm" """Create a new instance with new default values""" return PymunkHitBoxAlgorithm(detail=detail or self.detail) - def calculate(self, image: Image, detail: Optional[float] = None, **kwargs) -> PointList: + def calculate(self, image: Image, detail: Optional[float] = None, **kwargs) -> Point2List: """ Given an RGBA image, this returns points that make up a hit box around it. @@ -62,7 +62,7 @@ def calculate(self, image: Image, detail: Optional[float] = None, **kwargs) -> P return self.to_points_list(image, line_set) - def to_points_list(self, image: Image, line_set: List[Vec2d]) -> PointList: + def to_points_list(self, image: Image, line_set: List[Vec2d]) -> Point2List: """ Convert a line set to a list of points. @@ -100,7 +100,7 @@ def trace_image(self, image: Image) -> PolylineSet: :param image: Image to trace. :return: Line sets """ - def sample_func(sample_point: Point) -> int: + def sample_func(sample_point: Point2) -> int: """ Method used to sample image. """ if sample_point[0] < 0 \ or sample_point[1] < 0 \ diff --git a/arcade/hitbox/simple.py b/arcade/hitbox/simple.py index 5d1555dfa8..1cc496c14e 100644 --- a/arcade/hitbox/simple.py +++ b/arcade/hitbox/simple.py @@ -2,7 +2,7 @@ from typing import Tuple from PIL.Image import Image -from arcade.types import Point, PointList +from arcade.types import Point, Point2List from .base import HitBoxAlgorithm @@ -12,7 +12,7 @@ class SimpleHitBoxAlgorithm(HitBoxAlgorithm): from an image to create a hit box. """ - def calculate(self, image: Image, **kwargs) -> PointList: + def calculate(self, image: Image, **kwargs) -> Point2List: """ Given an RGBA image, this returns points that make up a hit box around it. Attempts to trim out transparent pixels. diff --git a/arcade/texture/texture.py b/arcade/texture/texture.py index 58ecf5d895..a6fef893f9 100644 --- a/arcade/texture/texture.py +++ b/arcade/texture/texture.py @@ -26,7 +26,7 @@ TransverseTransform, ) -from arcade.types import RGBA255, PointList +from arcade.types import RGBA255, Point2List from arcade.types.rect import Rect if TYPE_CHECKING: @@ -155,7 +155,7 @@ def __init__( image: Union[PIL.Image.Image, ImageData], *, hit_box_algorithm: Optional[HitBoxAlgorithm] = None, - hit_box_points: Optional[PointList] = None, + hit_box_points: Optional[Point2List] = None, hash: Optional[str] = None, **kwargs, ): @@ -192,7 +192,7 @@ def __init__( self._cache_name: str = "" self._atlas_name: str = "" self._update_cache_names() - self._hit_box_points: PointList = ( + self._hit_box_points: Point2List = ( hit_box_points or self._calculate_hit_box_points() ) @@ -393,7 +393,7 @@ def size(self, value: Tuple[int, int]): self._size = value @property - def hit_box_points(self) -> PointList: + def hit_box_points(self) -> Point2List: """ Get the hit box points for this texture. @@ -754,7 +754,7 @@ def validate_crop( if y + height - 1 >= image.height: raise ValueError(f"height is outside of texture: {height + y}") - def _calculate_hit_box_points(self) -> PointList: + def _calculate_hit_box_points(self) -> Point2List: """ Calculate the hit box points for this texture based on the configured hit box algorithm. This is usually done on texture creation diff --git a/arcade/texture/transforms.py b/arcade/texture/transforms.py index e933828cd4..dee3d589b2 100644 --- a/arcade/texture/transforms.py +++ b/arcade/texture/transforms.py @@ -10,7 +10,7 @@ from typing import Dict, Tuple from enum import Enum from arcade.math import rotate_point -from arcade.types import PointList +from arcade.types import Point2List class VertexOrder(Enum): @@ -42,8 +42,8 @@ class Transform: @staticmethod def transform_hit_box_points( - points: PointList, - ) -> PointList: + points: Point2List, + ) -> Point2List: """Transforms hit box points.""" return points @@ -99,8 +99,8 @@ class Rotate90Transform(Transform): @staticmethod def transform_hit_box_points( - points: PointList, - ) -> PointList: + points: Point2List, + ) -> Point2List: return tuple(rotate_point(point[0], point[1], 0, 0, 90) for point in points) @@ -117,8 +117,8 @@ class Rotate180Transform(Transform): @staticmethod def transform_hit_box_points( - points: PointList, - ) -> PointList: + points: Point2List, + ) -> Point2List: return tuple(rotate_point(point[0], point[1], 0, 0, 180) for point in points) @@ -134,8 +134,8 @@ class Rotate270Transform(Transform): ) @staticmethod def transform_hit_box_points( - points: PointList, - ) -> PointList: + points: Point2List, + ) -> Point2List: return tuple(rotate_point(point[0], point[1], 0, 0, 270) for point in points) @@ -152,8 +152,8 @@ class FlipLeftRightTransform(Transform): @staticmethod def transform_hit_box_points( - points: PointList, - ) -> PointList: + points: Point2List, + ) -> Point2List: return tuple((-point[0], point[1]) for point in points) @@ -170,8 +170,8 @@ class FlipTopBottomTransform(Transform): @staticmethod def transform_hit_box_points( - points: PointList, - ) -> PointList: + points: Point2List, + ) -> Point2List: return tuple((point[0], -point[1]) for point in points) @@ -188,8 +188,8 @@ class TransposeTransform(Transform): @staticmethod def transform_hit_box_points( - points: PointList, - ) -> PointList: + points: Point2List, + ) -> Point2List: points = FlipLeftRightTransform.transform_hit_box_points(points) points = Rotate270Transform.transform_hit_box_points(points) return points @@ -208,8 +208,8 @@ class TransverseTransform(Transform): @staticmethod def transform_hit_box_points( - points: PointList, - ) -> PointList: + points: Point2List, + ) -> Point2List: points = FlipLeftRightTransform.transform_hit_box_points(points) points = Rotate90Transform.transform_hit_box_points(points) return points diff --git a/arcade/tilemap/tilemap.py b/arcade/tilemap/tilemap.py index 43d8364773..cffee86f6e 100644 --- a/arcade/tilemap/tilemap.py +++ b/arcade/tilemap/tilemap.py @@ -38,7 +38,7 @@ from arcade.math import rotate_point from arcade.resources import resolve -from arcade.types import Point, TiledObject +from arcade.types import Point2, TiledObject _FLIPPED_HORIZONTALLY_FLAG = 0x80000000 _FLIPPED_VERTICALLY_FLAG = 0x40000000 @@ -510,7 +510,7 @@ def _create_sprite_from_tile( print("Warning, only one hit box supported for tile.") for hitbox in tile.objects.tiled_objects: - points: List[Point] = [] + points: List[Point2] = [] if isinstance(hitbox, pytiled_parser.tiled_object.Rectangle): if hitbox.size is None: print( @@ -587,7 +587,7 @@ def _create_sprite_from_tile( points = [(point[1], point[0]) for point in points] my_sprite.hit_box = RotatableHitBox( - cast(List[Point], points), + cast(List[Point2], points), position=my_sprite.position, angle=my_sprite.angle, scale=my_sprite.scale_xy, @@ -831,7 +831,7 @@ def _process_object_layer( sprite_list: Optional[SpriteList] = None objects_list: Optional[List[TiledObject]] = [] - shape: Union[List[Point], Tuple[int, int, int, int], Point, None] = None + shape: Union[List[Point2], Tuple[int, int, int, int], Point2, None] = None for cur_object in layer.tiled_objects: # shape: Optional[Union[Point, PointList, Rect]] = None @@ -965,7 +965,7 @@ def _process_object_layer( elif isinstance( cur_object, pytiled_parser.tiled_object.Polygon ) or isinstance(cur_object, pytiled_parser.tiled_object.Polyline): - points: List[Point] = [] + points: List[Point2] = [] shape = points for point in cur_object.points: x = point.x + cur_object.coordinates.x diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 7e507a15c7..f2642c45c8 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -163,7 +163,7 @@ # Speed / typing workaround: # 1. Eliminate extra allocations # 2. Allows type annotation to be cleaner, primarily for HitBox & subclasses -EMPTY_POINT_LIST: PointList = tuple() +EMPTY_POINT_LIST: Point2List = tuple() # Path handling PathLike = Union[str, Path, bytes] From 02354bd38efc9dc30d5a4113fc42963047073f62 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Thu, 30 May 2024 08:57:26 +1200 Subject: [PATCH 27/52] updated PR.md --- PR.md | 3 +++ arcade/hitbox/base.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/PR.md b/PR.md index 9586aba9a5..392c6597e8 100644 --- a/PR.md +++ b/PR.md @@ -14,6 +14,9 @@ - Added type aliases `Point2` and `Point3` - Camera - All camera functions now take `Point`, `Point2`, or `Point3` where points are expected + - All camera functions return `Vec2` or `Vec3` as expected + - Viewport has been removed from the ProjectionData Protocol + - Projectors now accept a viewport Rect, and a Scissor Rect - Added `Rect` and it's constructors, `Vec2`, and `Vec3` to top-level module - Added `Texture.draw_rect()` - Added `BasicSprite.rect` diff --git a/arcade/hitbox/base.py b/arcade/hitbox/base.py index 80d238b246..41b042eef1 100644 --- a/arcade/hitbox/base.py +++ b/arcade/hitbox/base.py @@ -3,12 +3,12 @@ from __future__ import annotations from math import cos, radians, sin -from typing import Any, Sequence, Tuple +from typing import Any, Tuple from typing_extensions import Self from PIL.Image import Image -from arcade.types import Point, Point2, Point2List, EMPTY_POINT_LIST +from arcade.types import Point2, Point2List, EMPTY_POINT_LIST __all__ = ["HitBoxAlgorithm", "HitBox", "RotatableHitBox"] From 8caf5042a4d0f92d0f3bd469b511900b65c3c03c Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Thu, 30 May 2024 09:09:22 +1200 Subject: [PATCH 28/52] hey pos is vec! --- arcade/camera/camera_2d.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 8f455870b4..de845326f6 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -266,7 +266,7 @@ def top_left(self) -> Vec2: top = self.top left = self.left - return Vec2(pos[0] + up[0] * top + up[1] * left, pos[1] + up[1] * top - up[0] * left) + return Vec2(pos.x + up[0] * top + up[1] * left, pos.y + up[1] * top - up[0] * left) @top_left.setter def top_left(self, new_corner: Point2): @@ -288,7 +288,7 @@ def top_center(self) -> Vec2: pos = self.position up = self._camera_data.up top = self.top - return Vec2(pos[0] + up[0] * top, pos[1] + up[1] * top) + return Vec2(pos.x + up[0] * top, pos.y + up[1] * top) @top_center.setter def top_center(self, new_top: Point2): @@ -308,7 +308,7 @@ def top_right(self) -> Vec2: top = self.top right = self.right - return Vec2(pos[0] + up[0] * top + up[1] * right, pos[1] + up[1] * top - up[0] * right) + return Vec2(pos.x + up[0] * top + up[1] * right, pos.y + up[1] * top - up[0] * right) @top_right.setter def top_right(self, new_corner: Point2): @@ -332,7 +332,7 @@ def bottom_right(self) -> Vec2: bottom = self.bottom right = self.right - return Vec2(pos[0] + up[0] * bottom + up[1] * right, pos[1] + up[1] * bottom - up[0] * right) + return Vec2(pos.x + up[0] * bottom + up[1] * right, pos.y + up[1] * bottom - up[0] * right) @bottom_right.setter def bottom_right(self, new_corner: Point2): @@ -355,7 +355,7 @@ def bottom_center(self) -> Vec2: up = self._camera_data.up bottom = self.bottom - return Vec2(pos[0] - up[0] * bottom, pos[1] - up[1] * bottom) + return Vec2(pos.x - up[0] * bottom, pos.y - up[1] * bottom) @bottom_center.setter def bottom_center(self, new_bottom: Point2): @@ -375,7 +375,7 @@ def bottom_left(self) -> Vec2: bottom = self.bottom left = self.left - return Vec2(pos[0] + up[0] * bottom + up[1] * left, pos[1] + up[1] * bottom - up[0] * left) + return Vec2(pos.x + up[0] * bottom + up[1] * left, pos.y + up[1] * bottom - up[0] * left) @bottom_left.setter def bottom_left(self, new_corner: Point2): @@ -397,7 +397,7 @@ def center_right(self) -> Vec2: pos = self.position up = self._camera_data.up right = self.right - return Vec2(pos[0] + up[1] * right, pos[1] - up[0] * right) + return Vec2(pos.x + up[1] * right, pos.y - up[0] * right) @center_right.setter def center_right(self, new_right: Point2): @@ -414,7 +414,7 @@ def center_left(self) -> Vec2: pos = self.position up = self._camera_data.up left = self.left - return Vec2(pos[0] + up[1] * left, pos[1] - up[0] * left) + return Vec2(pos.x + up[1] * left, pos.y - up[0] * left) @center_left.setter def center_left(self, new_left: Point2): From 610ad11312360ecf8b99041ab62e46d1a8f3b469 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 29 May 2024 16:27:06 -0500 Subject: [PATCH 29/52] Update from_camera_data docstring * Add scissor and viewport arguments as param entries * Rephrase existing doc a little --- arcade/camera/camera_2d.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index de845326f6..d24325c4d7 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -168,12 +168,22 @@ def from_camera_data(cls, *, :param camera_data: A :py:class:`~arcade.camera.data.CameraData` describing the position, up, forward and zoom. - :param projection_data: A :py:class:`~arcade.camera.data.OrthographicProjectionData` - which describes the left, right, top, bottom, far, near planes and the viewport - for an orthographic projection. - :param render_target: The FrameBuffer that the camera uses. Defaults to the screen. - If the framebuffer is not the default screen nothing drawn after this camera is used will - show up. The FrameBuffer's internal viewport is ignored. + :param projection_data: + A :py:class:`~arcade.camera.data.OrthographicProjectionData` + which describes the left, right, top, bottom, far, near + planes and the viewport for an orthographic projection. + :param render_target: A non-screen + :py:class:`~arcade.gl.framebuffer.Framebuffer` for this + camera to draw into. When specified, + + * nothing will draw directly to the screen + * the buffer's internal viewport will be ignored + + :param viewport: + A viewport as a :py:class:`~arcade.types.rect.Rect`. + This overrides any viewport the ``render_target`` may have. + :param scissor: + The OpenGL scissor box to use when drawing. :param window: The Arcade Window to bind the camera to. Defaults to the currently active window. """ From 3cbb1c4fa6616e30f146bfa233b062da2dcabf83 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 29 May 2024 17:32:26 -0400 Subject: [PATCH 30/52] Remove PR.md --- PR.md | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 PR.md diff --git a/PR.md b/PR.md deleted file mode 100644 index 392c6597e8..0000000000 --- a/PR.md +++ /dev/null @@ -1,27 +0,0 @@ -# The Rect, Part II -## The Rect-oning and the Vec-oning - -- `Rect` - - Added `Rect.distance_from_bounds()` - - Added `point in rect` support for `Rect` - - Added `*` and `/` support for scaling relative to `(0, 0)`. - - Added bool() support (area is not 0) - - Added support for round(), floor(), and ceil() - - Added `.area` property - - Functions expecting `Vec2` now accept `Tuple[AsFloat, AsFloat]` - - Improved docstrings - - Fixed `.viewport` -- Added type aliases `Point2` and `Point3` -- Camera - - All camera functions now take `Point`, `Point2`, or `Point3` where points are expected - - All camera functions return `Vec2` or `Vec3` as expected - - Viewport has been removed from the ProjectionData Protocol - - Projectors now accept a viewport Rect, and a Scissor Rect -- Added `Rect` and it's constructors, `Vec2`, and `Vec3` to top-level module -- Added `Texture.draw_rect()` -- Added `BasicSprite.rect` -- Added `Section.rect` -- Added `SpriteSolidColor.from_rect()` -- Added `NinePatchTexture.from_rect()` -- Remove `IntRect`, `FloatRect`, `RectList` -- Rename the old `Rect` to `GUIRect` From 415474f45c8ecd4cf68b77fef9d4c70bae91a9fa Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 29 May 2024 17:51:47 -0400 Subject: [PATCH 31/52] add UV helpers and Window.rect --- arcade/application.py | 6 ++++++ arcade/types/rect.py | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/arcade/application.py b/arcade/application.py index 9d6871d874..f10092e1ad 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -22,6 +22,7 @@ from arcade.context import ArcadeContext from arcade.types import Color, RGBOrA255, RGBANormalized from arcade import SectionManager +from arcade.types.rect import LBWH, Rect from arcade.utils import is_raspberry_pi LOG = logging.getLogger(__name__) @@ -311,6 +312,11 @@ def background_color(self) -> Color: """ return self._background_color + @property + def rect(self) -> Rect: + """Return a Rect describing the size of the window.""" + return LBWH(0, 0, self.width, self.height) + @background_color.setter def background_color(self, value: RGBOrA255): self._background_color = Color.from_iterable(value) diff --git a/arcade/types/rect.py b/arcade/types/rect.py index 1a0f19360b..194bf6ba16 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -154,8 +154,7 @@ def move(self, dx: AsFloat = 0.0, dy: AsFloat = 0.0) -> Rect: def resize(self, width: Optional[AsFloat] = None, height: Optional[AsFloat] = None, - anchor: Vec2 = AnchorPoint.CENTER - ) -> Rect: + anchor: Vec2 = AnchorPoint.CENTER) -> Rect: """ Returns a new :py:class:`~arcade.types.rect.Rect` at the current Rect's position, but with a new width and height, anchored at a point (default center.) @@ -393,6 +392,22 @@ def point_on_bounds(self, point: Point2, tolerance: float) -> bool: """Returns True if the given point is on the bounds of this rectangle within some tolerance.""" return abs(self.distance_from_bounds(point)) < tolerance + def position_to_uv(self, point: Point2) -> Vec2: + """Take an absolute point and translate it to it's relative position in UV-space (percentage across this rectangle.)""" + x, y = point + return Vec2( + (x - self.left) / self.width, + (y - self.bottom) / self.height, + ) + + def uv_to_position(self, uv: Point2) -> Vec2: + """Take a point in UV-space (percentage across this rectangle) and translate it to it's absolute position.""" + x, y = uv + return Vec2( + self.left + x * self.width, + self.bottom + y * self.height + ) + def to_points(self) -> tuple[Vec2, Vec2, Vec2, Vec2]: """Returns a tuple of the four corners of this Rect.""" left = self.left From 1fd7a2e641477e59b10e69c7b1f1a3b0cbeeb30f Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 29 May 2024 18:03:59 -0400 Subject: [PATCH 32/52] BasicSprite.scale_xy is a Vec2 now --- arcade/sprite/base.py | 4 +++- tests/unit/sprite/test_sprite.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 9f46f615be..7059968d57 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -9,6 +9,8 @@ from arcade.texture import Texture from arcade.utils import copy_dunders_unimplemented +from pyglet.math import Vec2 + if TYPE_CHECKING: from arcade.sprite_list import SpriteList @@ -60,7 +62,7 @@ def __init__( self._texture = texture self._width = texture.width * scale self._height = texture.height * scale - self._scale = scale, scale + self._scale = Vec2(scale, scale) self._visible = bool(visible) self._color: Color = WHITE self.sprite_lists: List["SpriteList"] = [] diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index 9e33b27c4a..f85ca8e557 100644 --- a/tests/unit/sprite/test_sprite.py +++ b/tests/unit/sprite/test_sprite.py @@ -1,6 +1,7 @@ import pytest as pytest import arcade +from pyglet.math import Vec2 frame_counter = 0 @@ -391,7 +392,7 @@ def test_sprite_scale_xy(window): sprite.scale = 1.0 sprite.scale_xy = (1.0, 1.0) assert sprite.scale == 1.0 - assert sprite.scale_xy == (1.0, 1.0) + assert sprite.scale_xy == Vec2(1.0, 1.0) assert sprite.width, sprite.height == (20, 20) # setting scale_xy to identical values in each channel works From fb39ca4315184c474972ab439b019ca1d4c2c613 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 29 May 2024 18:06:33 -0400 Subject: [PATCH 33/52] fix setter for .scale_xy --- arcade/sprite/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 7059968d57..5d2bf65318 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -223,7 +223,8 @@ def scale_xy(self, new_value: Point2): if new_value[0] == self._scale[0] and new_value[1] == self._scale[1]: return - self._scale = new_value + x, y = new_value + self._scale = Vec2(x, y) self._hit_box.scale = self._scale if self._texture: self._width = self._texture.width * self._scale[0] From 27d0ba85bca6f1d5612fb09680c4e32789473d6a Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 29 May 2024 18:12:21 -0400 Subject: [PATCH 34/52] make 3.8 happy --- arcade/types/vector_like.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arcade/types/vector_like.py b/arcade/types/vector_like.py index 8dc78786dc..7fd3cd0aea 100644 --- a/arcade/types/vector_like.py +++ b/arcade/types/vector_like.py @@ -7,14 +7,14 @@ """ from __future__ import annotations -from typing import Union +from typing import Union, Tuple from pyglet.math import Vec2, Vec3 from arcade.types.numbers import AsFloat -Point2 = Union[tuple[AsFloat, AsFloat], Vec2] -Point3 = Union[tuple[AsFloat, AsFloat, AsFloat], Vec3] +Point2 = Union[Tuple[AsFloat, AsFloat], Vec2] +Point3 = Union[Tuple[AsFloat, AsFloat, AsFloat], Vec3] class AnchorPoint: From b0fb4f55b536706feaa203ae4ef95cf5226d33c0 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 29 May 2024 18:21:16 -0400 Subject: [PATCH 35/52] finish scale_xy Vec2ificiation --- arcade/sprite/base.py | 2 +- tests/unit/sprite/test_sprite.py | 44 ++++++++++++++++---------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 5d2bf65318..7fb2338570 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -579,7 +579,7 @@ def rescale_xy_relative_to_point( return # set the scale and, if this sprite has a texture, the size data - self.scale_xy = self._scale[0] * factor_x, self._scale[1] * factor_y + self.scale_xy = Vec2(self._scale[0] * factor_x, self._scale[1] * factor_y) if self._texture: self._width = self._texture.width * self._scale[0] self._height = self._texture.height * self._scale[1] diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index f85ca8e557..0588c06157 100644 --- a/tests/unit/sprite/test_sprite.py +++ b/tests/unit/sprite/test_sprite.py @@ -49,7 +49,7 @@ def test_sprite(window: arcade.Window): character_sprite.angle = 90 character_list.append(character_sprite) - character_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png", scale= CHARACTER_SCALING) + character_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png", scale=CHARACTER_SCALING) character_sprite.center_x = 300 character_sprite.center_y = 50 character_sprite.angle = 180 @@ -260,6 +260,7 @@ def update(delta_time): frame = 0 + def test_sprite_removal(window): CHARACTER_SCALING = 0.5 global frame @@ -283,7 +284,6 @@ def test_sprite_removal(window): sprite_3.center_y = 250 character_list.append(sprite_3) - def on_draw(): arcade.start_render() character_list.draw() @@ -345,13 +345,13 @@ def test_sprite_rgb_property_basics(): # Values which are too short are not allowed with pytest.raises(ValueError): - sprite.rgb = (1,2) + sprite.rgb = (1, 2) with pytest.raises(ValueError): sprite.rgb = (0,) # Nor are values which are too long with pytest.raises(ValueError): - sprite.rgb = (100,100,100,100,100) + sprite.rgb = (100, 100, 100, 100, 100) # Test color setting + .rgb report when .visible == True sprite.rgb = (1, 3, 5, 7) @@ -398,18 +398,18 @@ def test_sprite_scale_xy(window): # setting scale_xy to identical values in each channel works sprite.scale_xy = 2.0, 2.0 assert sprite.scale == 2.0 - assert sprite.scale_xy == (2.0, 2.0) + assert sprite.scale_xy == Vec2(2.0, 2.0) assert sprite.width, sprite.height == (40, 40) # setting scale_xy with x < y scale works correctly sprite.scale_xy = 1.0, 4.0 - assert sprite.scale_xy == (1.0, 4.0) + assert sprite.scale_xy == Vec2(1.0, 4.0) assert sprite.scale == 1.0 assert sprite.width, sprite.height == (20, 80) # setting scale_xy with x > y scale works correctly sprite.scale_xy = 5.0, 3.0 - assert sprite.scale_xy == (5.0, 3.0) + assert sprite.scale_xy == Vec2(5.0, 3.0) assert sprite.scale == 5.0 assert sprite.width, sprite.height == (100, 60) @@ -439,7 +439,7 @@ def test_sprite_scale_resets_mismatched_xy_settings(window): sprite.scale_xy = 3.0, 2.0 sprite.scale = 2.0 assert sprite.scale == 2.0 - assert sprite.scale_xy == (2.0, 2.0) + assert sprite.scale_xy == Vec2(2.0, 2.0) assert sprite.width == 40 assert sprite.height == 40 @@ -447,7 +447,7 @@ def test_sprite_scale_resets_mismatched_xy_settings(window): sprite.scale_xy = 5.0, 3.0 sprite.scale = 5.0 assert sprite.scale == 5.0 - assert sprite.scale_xy == (5.0, 5.0) + assert sprite.scale_xy == Vec2(5.0, 5.0) assert sprite.width == 100 assert sprite.height == 100 @@ -455,7 +455,7 @@ def test_sprite_scale_resets_mismatched_xy_settings(window): sprite.scale_xy = 0.5, 4.0 sprite.scale = 1.0 assert sprite.scale == 1.0 - assert sprite.scale_xy == (1.0, 1.0) + assert sprite.scale_xy == Vec2(1.0, 1.0) assert sprite.width == 20 assert sprite.height == 20 @@ -463,28 +463,28 @@ def test_sprite_scale_resets_mismatched_xy_settings(window): sprite.scale_xy = 0.5, 4.0 sprite.scale = -1.0 assert sprite.scale == -1.0 - assert sprite.scale_xy == (-1.0, -1.0) + assert sprite.scale_xy == Vec2(-1.0, -1.0) assert sprite.width == -20 assert sprite.height == -20 # edge case: x scale < 0 is reset to positive sprite.scale_xy = -1.0, 1.0 sprite.scale = 2.0 - assert sprite.scale_xy == (2.0, 2.0) + assert sprite.scale_xy == Vec2(2.0, 2.0) assert sprite.width == 40 assert sprite.height == 40 # edge case: y scale < 0 is reset to positive sprite.scale_xy = 1.0, -1.0 sprite.scale = 2.0 - assert sprite.scale_xy == (2.0, 2.0) + assert sprite.scale_xy == Vec2(2.0, 2.0) assert sprite.width == 40 assert sprite.height == 40 # edge case: x < 0, y < 0 is reset to positive sprite.scale_xy = -1.0, -1.0 sprite.scale = 2.0 - assert sprite.scale_xy == (2.0, 2.0) + assert sprite.scale_xy == Vec2(2.0, 2.0) assert sprite.width == 40 assert sprite.height == 40 @@ -532,7 +532,7 @@ def sprite_64x64_at_position(x, y): sprite_2.scale_xy = 2.0, 1.0 sprite_2.rescale_relative_to_point(window_center, 2.0) assert sprite_2.scale == 4.0 - assert sprite_2.scale_xy == (4.0, 2.0) + assert sprite_2.scale_xy == Vec2(4.0, 2.0) assert sprite_2.center_x == window_center_x + 20 assert sprite_2.center_y == window_center_y + 20 assert sprite_2.width == 256 @@ -551,7 +551,7 @@ def sprite_64x64_at_position(x, y): ) sprite_3.scale_xy = 0.5, 1.5 sprite_3.rescale_relative_to_point(window_center, 3.0) - assert sprite_3.scale_xy == (1.5, 4.5) + assert sprite_3.scale_xy == Vec2(1.5, 4.5) assert sprite_3.center_x == window_center_x - 30 assert sprite_3.center_y == window_center_y - 30 assert sprite_3.width == 96 @@ -562,7 +562,7 @@ def sprite_64x64_at_position(x, y): sprite_4 = sprite_64x64_at_position(*window_center) sprite_4.rescale_relative_to_point(sprite_4.position, 2.0) assert sprite_4.scale == 2.0 - assert sprite_4.scale_xy == (2.0, 2.0) + assert sprite_4.scale_xy == Vec2(2.0, 2.0) assert sprite_4.center_x == window_center_x assert sprite_4.center_y == window_center_y assert sprite_4.width == 128 @@ -573,7 +573,7 @@ def sprite_64x64_at_position(x, y): sprite_5 = sprite_64x64_at_position(*window_center) sprite_5.rescale_relative_to_point(sprite_5.position, -2.0) assert sprite_5.scale == -2.0 - assert sprite_5.scale_xy == (-2.0, -2.0) + assert sprite_5.scale_xy == Vec2(-2.0, -2.0) assert sprite_5.center_x == window_center_x assert sprite_5.center_y == window_center_y assert sprite_5.width == -128 @@ -638,7 +638,7 @@ def sprite_64x64_at_position(x, y): ) sprite_1.rescale_xy_relative_to_point((0, 0), (3.31, 3.31)) assert sprite_1.scale == 3.31 - assert sprite_1.scale_xy == (3.31, 3.31) + assert sprite_1.scale_xy == Vec2(3.31, 3.31) assert sprite_1.center_x == (window_center_x + 50) * 3.31 assert sprite_1.center_y == (window_center_y - 50) * 3.31 assert sprite_1.width == 64 * 3.31 @@ -652,7 +652,7 @@ def sprite_64x64_at_position(x, y): sprite_2.scale_xy = 2.0, 1.0 sprite_2.rescale_xy_relative_to_point(window_center, (2.0, 2.0)) assert sprite_2.scale == 4.0 - assert sprite_2.scale_xy == (4.0, 2.0) + assert sprite_2.scale_xy == Vec2(4.0, 2.0) assert sprite_2.center_x == window_center_x + 20 assert sprite_2.center_y == window_center_y + 20 assert sprite_2.width == 256 @@ -666,7 +666,7 @@ def sprite_64x64_at_position(x, y): sprite_3.scale_xy = 0.5, 1.5 sprite_3.rescale_xy_relative_to_point(window_center, (3.0, 3.0)) assert sprite_3.scale == 1.5 - assert sprite_3.scale_xy == (1.5, 4.5) + assert sprite_3.scale_xy == Vec2(1.5, 4.5) assert sprite_3.center_x == window_center_x - 30 assert sprite_3.center_y == window_center_y - 30 assert sprite_3.width == 96 @@ -700,7 +700,7 @@ def sprite_64x64_at_position(x, y): sprite_6 = sprite_64x64_at_position(*window_center) sprite_6.rescale_xy_relative_to_point(sprite_6.position, (-2.0, -2.0)) assert sprite_6.scale == -2.0 - assert sprite_6.scale_xy == (-2.0, -2.0) + assert sprite_6.scale_xy == Vec2(-2.0, -2.0) assert sprite_6.center_x == window_center_x assert sprite_6.center_y == window_center_y assert sprite_6.width == -128 From dd48d37830361801b8a7a2bf54ae5b5cf4f144f5 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 29 May 2024 19:13:54 -0400 Subject: [PATCH 36/52] oops, forgot to cast to Vec2 --- arcade/sprite/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 7fb2338570..c6f82c1fc0 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -203,7 +203,7 @@ def scale(self, new_value: float): if new_value == self._scale[0] and new_value == self._scale[1]: return - self._scale = new_value, new_value + self._scale = Vec2(new_value, new_value) self._hit_box.scale = self._scale if self._texture: self._width = self._texture.width * self._scale[0] From ee82ceaaa5736c43e474f7c505d58e59b57906a2 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Wed, 29 May 2024 19:40:41 -0500 Subject: [PATCH 37/52] Improve some Rect doc * Fix sphinx literal issue + document Rect.kwargs * Improve RectKwargs doc --- arcade/types/rect.py | 51 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/arcade/types/rect.py b/arcade/types/rect.py index 194bf6ba16..a2a157eb81 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -15,10 +15,18 @@ class RectKwargs(TypedDict): - """A dictionary of the eight canon properties of a rectangle. + """Annotates an ordinary :py:class:`dict` of :py:class:`Rect` arguments. + + This is only meaningful as a type annotation during type checking. + For example, the :py:meth:`Rect.kwargs ` + property returns an ordinary will actually be a :py:class:`dict` + of :py:class:`Rect` field names to :py:class:`float` values. + + To learn more, please see: + + * :py:class:`.Rect` + * :py:class:`typing.TypedDict` - ``left``, ``right``, ``bottom``, ``top``, - ``width``, ``height``, ``x``, and ``y`` are all :py:class:`float`s. """ left: float right: float @@ -477,6 +485,43 @@ def from_kwargs(cls, **kwargs: AsFloat) -> Rect: @property def kwargs(self) -> RectKwargs: + """Get this rectangle as a :py:class:`dict` of field names to values. + + .. _tomli: https://github.com/hukkin/tomli + + Many data formats have corresponding Python modules with write + support. Such modules often one or more functions which convert + a passed :py:class:`dict` to a :py:class:`str` or write the result + of such a conversion to a file. + + For example, the built-in :py:mod:`json` module offers the + following functions on all Python versions currently supported + by Arcade: + + .. list-table:: + :header-rows: 1 + + * - Function + - Summary + - Useful For + + * - :py:func:`json.dump` + - Write a :py:class:`dict` to a file + - Saving game progress or edited levels + + * - :py:func:`json.dumps` + - Get a :py:class:`dict` as a :py:class:`str` of JSON + - Calls to a Web API + + .. note:: + + The return value is an ordinary :py:class:`dict`. + + Although the return type is annotated as a + :py:class:`.RectKwargs`, it is only meaningful when type + checking. See :py:class:`typing.TypedDict` to learn more. + + """ return {"left": self.left, "right": self.right, "bottom": self.bottom, From 5f96813098f4bf020f8b8eea10f1d7f90e7b2284 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 29 May 2024 20:59:24 -0400 Subject: [PATCH 38/52] E501 can kick the bucket --- arcade/types/rect.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/arcade/types/rect.py b/arcade/types/rect.py index a2a157eb81..2416b789ac 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -401,7 +401,9 @@ def point_on_bounds(self, point: Point2, tolerance: float) -> bool: return abs(self.distance_from_bounds(point)) < tolerance def position_to_uv(self, point: Point2) -> Vec2: - """Take an absolute point and translate it to it's relative position in UV-space (percentage across this rectangle.)""" + """Take an absolute point and translate it to it's relative position in UV-space + (percentage across this rectangle.) + """ x, y = point return Vec2( (x - self.left) / self.width, @@ -409,7 +411,9 @@ def position_to_uv(self, point: Point2) -> Vec2: ) def uv_to_position(self, uv: Point2) -> Vec2: - """Take a point in UV-space (percentage across this rectangle) and translate it to it's absolute position.""" + """Take a point in UV-space (percentage across this rectangle) and translate it + to it's absolute position. + """ x, y = uv return Vec2( self.left + x * self.width, From b717d64e0eca4ec167b96d7ee1d3828a819d1d02 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 30 May 2024 12:04:12 -0500 Subject: [PATCH 39/52] Fix mypy issues with background_color by moving setter back to where it was --- arcade/application.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/arcade/application.py b/arcade/application.py index f10092e1ad..c86b7efb61 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -312,15 +312,15 @@ def background_color(self) -> Color: """ return self._background_color + @background_color.setter + def background_color(self, value: RGBOrA255): + self._background_color = Color.from_iterable(value) + @property def rect(self) -> Rect: """Return a Rect describing the size of the window.""" return LBWH(0, 0, self.width, self.height) - @background_color.setter - def background_color(self, value: RGBOrA255): - self._background_color = Color.from_iterable(value) - def run(self) -> None: """ Run the main loop. From 3b0c842eb2ad3089463db0236dc823690cb24e19 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 30 May 2024 12:34:18 -0500 Subject: [PATCH 40/52] Silence error for an intentional Liskov principle violation on __mul__ --- arcade/types/rect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/types/rect.py b/arcade/types/rect.py index 2416b789ac..d7bffb782e 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -215,7 +215,7 @@ def scale_axes(self, new_scale: Point2, anchor: Vec2 = AnchorPoint.CENTER) -> Re return LRBT(adjusted_left, adjusted_right, adjusted_bottom, adjusted_top) - def __mul__(self, scale: AsFloat) -> Rect: + def __mul__(self, scale: AsFloat) -> Rect: # type: ignore[override] """Scale the Rect by ``scale`` relative to ``(0, 0)``.""" return Rect(self.left * scale, self.right * scale, self.bottom * scale, self.top * scale, self.width * scale, self.height * scale, self.x * scale, self.y * scale) From fe72c5a1dd3265fca12bd019b1fa0469d064eb8b Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 30 May 2024 13:07:27 -0500 Subject: [PATCH 41/52] Attempt to fix pyright issues with generics --- arcade/camera/data_types.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/arcade/camera/data_types.py b/arcade/camera/data_types.py index 1f77636283..28afa59252 100644 --- a/arcade/camera/data_types.py +++ b/arcade/camera/data_types.py @@ -4,14 +4,13 @@ wide usage throughout Arcade's camera code. """ from __future__ import annotations -from typing import Protocol, Tuple, Generator +from typing import Protocol, Tuple, Generator, TypeVar from contextlib import contextmanager from typing_extensions import Self from pyglet.math import Vec2, Vec3 -from arcade.types import Point, Point3, Rect, LRBT, AsFloat - +from arcade.types import Point, Point3, Rect, LRBT, AsFloat, Point2 __all__ = [ 'CameraData', @@ -312,8 +311,11 @@ class Projection(Protocol): near: float far: float +_P = TypeVar('_P') + + +class Projector(Protocol[_P]): -class Projector(Protocol): """Projects from world coordinates to viewport pixel coordinates. Projectors also support converting in the opposite direction from @@ -376,13 +378,13 @@ def use(self) -> None: def activate(self) -> Generator[Self, None, None]: ... - def project(self, world_coordinate: Point) -> Vec2: + def project(self, world_coordinate: _P) -> Vec2: """ Take a Vec2 or Vec3 of coordinates and return the related screen coordinate """ ... - def unproject(self, screen_coordinate: Point) -> Vec3: + def unproject(self, screen_coordinate: Point2) -> _P: """ Take in a pixel coordinate and return the associated world coordinate From 50276ca7db31211dacba7bda4c1c0086c55b5359 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 30 May 2024 13:22:41 -0500 Subject: [PATCH 42/52] Fix ruff linting issue and clean up some imports * Remove unused Point import in camera/data_types.py * Add a newline before __all__ * Reorder some imports --- arcade/camera/data_types.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/arcade/camera/data_types.py b/arcade/camera/data_types.py index 28afa59252..903bc545f5 100644 --- a/arcade/camera/data_types.py +++ b/arcade/camera/data_types.py @@ -4,13 +4,14 @@ wide usage throughout Arcade's camera code. """ from __future__ import annotations -from typing import Protocol, Tuple, Generator, TypeVar from contextlib import contextmanager +from typing import Protocol, Tuple, Generator, TypeVar from typing_extensions import Self from pyglet.math import Vec2, Vec3 -from arcade.types import Point, Point3, Rect, LRBT, AsFloat, Point2 +from arcade.types import AsFloat, Point2, Point3, Rect, LRBT + __all__ = [ 'CameraData', From 8ecb1e9b874c95bdea1fca3c3ce3b63dd4d36f4b Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 31 May 2024 11:52:08 +1200 Subject: [PATCH 43/52] Fixed pyright completely --- arcade/camera/projection_functions.py | 5 +++-- arcade/camera/static.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/arcade/camera/projection_functions.py b/arcade/camera/projection_functions.py index 65a860a09d..51946a4c4e 100644 --- a/arcade/camera/projection_functions.py +++ b/arcade/camera/projection_functions.py @@ -131,10 +131,11 @@ def unproject_orthographic(screen_coordinate: Point, return Vec3(_world_position.x, _world_position.y, _world_position.z) -def project_perspective(world_coordinate: Point3, +def project_perspective(world_coordinate: Point, viewport: Tuple[int, int, int, int], view_matrix: Mat4, projection_matrix: Mat4) -> Vec2: - x, y, z = world_coordinate + x, y, *z = world_coordinate + z = 1.0 if not z else z[0] world_position = Vec4(x, y, z, 1.0) diff --git a/arcade/camera/static.py b/arcade/camera/static.py index bc69f84e98..13d21759dc 100644 --- a/arcade/camera/static.py +++ b/arcade/camera/static.py @@ -174,8 +174,8 @@ def static_from_matrices( viewport: Optional[Tuple[int, int, int, int]], *, window: Optional[Window] = None, - project_method: Optional[Callable[[Vec3, Tuple[int, int, int, int], Mat4, Mat4], Vec2]] = None, - unproject_method: Optional[Callable[[Vec2, Tuple[int, int, int, int], Mat4, Mat4], Vec3]] = None + project_method: Optional[Callable[[Point, Tuple[int, int, int, int], Mat4, Mat4], Vec2]] = None, + unproject_method: Optional[Callable[[Point, Tuple[int, int, int, int], Mat4, Mat4], Vec3]] = None ) -> _StaticCamera: return _StaticCamera(view, projection, viewport, window=window, project_method=project_method, unproject_method=unproject_method) From 041af6324c973c82d49a72e2d1eb02d107cfcb92 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 31 May 2024 11:52:55 +1200 Subject: [PATCH 44/52] ruff fix --- arcade/camera/projection_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/camera/projection_functions.py b/arcade/camera/projection_functions.py index 51946a4c4e..857d0a4236 100644 --- a/arcade/camera/projection_functions.py +++ b/arcade/camera/projection_functions.py @@ -3,7 +3,7 @@ from pyglet.math import Vec2, Vec3, Vec4, Mat4 from arcade.camera.data_types import CameraData, PerspectiveProjectionData, OrthographicProjectionData -from arcade.types import Point, Point3 +from arcade.types import Point def generate_view_matrix(camera_data: CameraData) -> Mat4: From 3240051c6d739e5bb3e53c0ee98e5bc8fb44f5ef Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 30 May 2024 19:33:18 -0500 Subject: [PATCH 45/52] Revert generic projector protocol * Remove generic projector * Clean up imports --- arcade/camera/data_types.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/arcade/camera/data_types.py b/arcade/camera/data_types.py index 903bc545f5..c9896d0e51 100644 --- a/arcade/camera/data_types.py +++ b/arcade/camera/data_types.py @@ -5,13 +5,12 @@ """ from __future__ import annotations from contextlib import contextmanager -from typing import Protocol, Tuple, Generator, TypeVar +from typing import Protocol, Tuple, Generator from typing_extensions import Self from pyglet.math import Vec2, Vec3 -from arcade.types import AsFloat, Point2, Point3, Rect, LRBT - +from arcade.types import AsFloat, Point, Point3, Rect, LRBT __all__ = [ 'CameraData', @@ -312,10 +311,9 @@ class Projection(Protocol): near: float far: float -_P = TypeVar('_P') -class Projector(Protocol[_P]): +class Projector(Protocol): """Projects from world coordinates to viewport pixel coordinates. @@ -379,13 +377,13 @@ def use(self) -> None: def activate(self) -> Generator[Self, None, None]: ... - def project(self, world_coordinate: _P) -> Vec2: + def project(self, world_coordinate: Point) -> Vec2: """ Take a Vec2 or Vec3 of coordinates and return the related screen coordinate """ ... - def unproject(self, screen_coordinate: Point2) -> _P: + def unproject(self, screen_coordinate: Point) -> Vec3: """ Take in a pixel coordinate and return the associated world coordinate From 57265a7bc95bf0b2eab62f286cf753a1fa07824d Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 30 May 2024 20:10:47 -0500 Subject: [PATCH 46/52] Add comments explaining uses @alejcas asked about * Comment arcade.types.Point * Move the temporary arcade.types.Velocity stub --- arcade/types/__init__.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index f2642c45c8..06290dfbaf 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -150,21 +150,32 @@ #: ... # No function definition #: Size2D = Tuple[_T, _T] -IPoint = Tuple[int, int] -Point = Union[Point2, Point3] +#: Used in :py:class:`~arcade.sprite_list.spatial_hash.SpatialHash`. +IPoint = Tuple[int, int] -# We won't keep this forever. It's a temp stub for particles we'll replace. -Velocity = Tuple[AsFloat, AsFloat] +#: Matches any 2D or 3D point, including: +#: +#: * :py:class:`pyglet.math.Vec2` +#: * :py:class:`pyglet.math.Vec3` +#: * An ordinary :py:class:`tuple` of 2 or 3 :py:class:`float` values +#: +#: This works the same way that :py:attr:`arcade.types.RGBA255` matches +#: either a :py:class:`tuple` or a :py:class:`Color`. +Point = Union[Point2, Point3] PointList = Sequence[Point] Point2List = Sequence[Point2] Point3List = Sequence[Point3] + # Speed / typing workaround: # 1. Eliminate extra allocations # 2. Allows type annotation to be cleaner, primarily for HitBox & subclasses EMPTY_POINT_LIST: Point2List = tuple() +# We won't keep this forever. It's a temp stub for particles we'll replace. +Velocity = Tuple[AsFloat, AsFloat] + # Path handling PathLike = Union[str, Path, bytes] _POr = TypeVar('_POr') # Allows PathOr[TypeNameHere] syntax From eeb4612806a2b5915d2b4e9f83bcc9bd9f476623 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 30 May 2024 20:48:30 -0500 Subject: [PATCH 47/52] Document AnchorPoint more clearly --- arcade/types/vector_like.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/arcade/types/vector_like.py b/arcade/types/vector_like.py index 7fd3cd0aea..cb880ad300 100644 --- a/arcade/types/vector_like.py +++ b/arcade/types/vector_like.py @@ -7,6 +7,7 @@ """ from __future__ import annotations + from typing import Union, Tuple from pyglet.math import Vec2, Vec3 @@ -18,7 +19,21 @@ class AnchorPoint: - """Provides helper aliases for several Vec2s to be used as anchor points in UV space.""" + """Common anchor points as constants in UV space. + + Each is a :py:class:`~pyglet.math.Vec2` with axis values between + ``0.0`` and ``1.0``. They can be used as arguments to + :py:meth:`Rect.uv_to_position ` + to help calculate: + + * a pixel offset inside a :py:class:`~arcade.types.Rect` + * an absolute screen positions in pixels + + Advanced users may also find them useful when working with + :ref:`tutorials_shaders`. + """ + + BOTTOM_LEFT = Vec2(0.0, 0.0) BOTTOM_CENTER = Vec2(0.5, 0.0) BOTTOM_RIGHT = Vec2(1.0, 0.0) From 1d6d2fcf1e5cbbac9ec32f5b96bf16c2abd7dac3 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 30 May 2024 21:00:51 -0500 Subject: [PATCH 48/52] Move non-obsolete vector-like annotations to vector_like * Move Point*List into vector_like * Fix __all__s and imports * Mark potentially obsolete items with comments --- arcade/types/__init__.py | 35 +++++++++++++------------------ arcade/types/vector_like.py | 41 +++++++++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/arcade/types/__init__.py b/arcade/types/__init__.py index 06290dfbaf..3c56bde7ff 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -28,7 +28,6 @@ from typing import ( NamedTuple, Optional, - Sequence, Tuple, Union, TYPE_CHECKING, @@ -75,8 +74,15 @@ # The Color helper type from arcade.types.color import Color -# We'll be moving our Vec-like items into this (Points, Sizes, etc) -from arcade.types.vector_like import AnchorPoint, Point2, Point3 +# Vector-like items and collections +from arcade.types.vector_like import Point2 +from arcade.types.vector_like import Point3 +from arcade.types.vector_like import Point +from arcade.types.vector_like import Point2List +from arcade.types.vector_like import Point3List +from arcade.types.vector_like import PointList +from arcade.types.vector_like import EMPTY_POINT_LIST +from arcade.types.vector_like import AnchorPoint # Rectangles from arcade.types.rect import ViewportParams @@ -132,6 +138,8 @@ _T = TypeVar('_T') +# --- Begin potentially obsolete annotations --- + #: ``Size2D`` helps mark int or float sizes. Use it like a #: :py:class:`typing.Generic`'s bracket notation as follows: #: @@ -154,28 +162,13 @@ #: Used in :py:class:`~arcade.sprite_list.spatial_hash.SpatialHash`. IPoint = Tuple[int, int] -#: Matches any 2D or 3D point, including: -#: -#: * :py:class:`pyglet.math.Vec2` -#: * :py:class:`pyglet.math.Vec3` -#: * An ordinary :py:class:`tuple` of 2 or 3 :py:class:`float` values -#: -#: This works the same way that :py:attr:`arcade.types.RGBA255` matches -#: either a :py:class:`tuple` or a :py:class:`Color`. -Point = Union[Point2, Point3] - -PointList = Sequence[Point] -Point2List = Sequence[Point2] -Point3List = Sequence[Point3] - -# Speed / typing workaround: -# 1. Eliminate extra allocations -# 2. Allows type annotation to be cleaner, primarily for HitBox & subclasses -EMPTY_POINT_LIST: Point2List = tuple() # We won't keep this forever. It's a temp stub for particles we'll replace. Velocity = Tuple[AsFloat, AsFloat] +# --- End potentially obsolete annotations --- + + # Path handling PathLike = Union[str, Path, bytes] _POr = TypeVar('_POr') # Allows PathOr[TypeNameHere] syntax diff --git a/arcade/types/vector_like.py b/arcade/types/vector_like.py index cb880ad300..209a4a2c6f 100644 --- a/arcade/types/vector_like.py +++ b/arcade/types/vector_like.py @@ -1,4 +1,4 @@ -"""This will hold point, size, and other similar aliases. +"""Points, sizes, and other similar aliases. This is a submodule of :py:mod:`arcade.types` to avoid issues with: @@ -8,16 +8,44 @@ """ from __future__ import annotations -from typing import Union, Tuple +from typing import Union, Tuple, Sequence from pyglet.math import Vec2, Vec3 from arcade.types.numbers import AsFloat +#: Matches both :py:class:`~pyglet.math.Vec2` and tuples of two numbers. Point2 = Union[Tuple[AsFloat, AsFloat], Vec2] + +#: Matches both :py:class:`~pyglet.math.Vec3` and tuples of three numbers. Point3 = Union[Tuple[AsFloat, AsFloat, AsFloat], Vec3] +#: Matches any 2D or 3D point, including: +#: +#: * :py:class:`pyglet.math.Vec2` +#: * :py:class:`pyglet.math.Vec3` +#: * An ordinary :py:class:`tuple` of 2 or 3 values, either: +#: +#: * :py:class:`int` +# * :py:class:`float` +#: +#: This works the same way as :py:attr:`arcade.types.RGBOrA255` to +#: annotate RGB tuples, RGBA tuples, and :py:class:`tuple` or a +#: :py:class:`Color` instances. +Point = Union[Point2, Point3] + +PointList = Sequence[Point] +Point2List = Sequence[Point2] +Point3List = Sequence[Point3] + + +# Speed / typing workaround: +# 1. Eliminate extra allocations +# 2. Allows type annotation to be cleaner, primarily for HitBox & subclasses +EMPTY_POINT_LIST: Point2List = tuple() + + class AnchorPoint: """Common anchor points as constants in UV space. @@ -46,7 +74,12 @@ class AnchorPoint: __all__ = [ - 'AnchorPoint', 'Point2', - 'Point3' + 'Point3', + 'Point', + 'Point2List', + 'Point3List', + 'PointList', + 'AnchorPoint', + 'EMPTY_POINT_LIST' ] From 79e7da05437233e4033b795864c0cf8849d24e2f Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Thu, 30 May 2024 22:45:13 -0400 Subject: [PATCH 49/52] sorry push I want this to be green before I go to bed --- arcade/types/vector_like.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/types/vector_like.py b/arcade/types/vector_like.py index 209a4a2c6f..0ae5ef8429 100644 --- a/arcade/types/vector_like.py +++ b/arcade/types/vector_like.py @@ -58,7 +58,7 @@ class AnchorPoint: * an absolute screen positions in pixels Advanced users may also find them useful when working with - :ref:`tutorials_shaders`. + shaders. """ From 100897d2defb96d17e0792bd35a507d88ab9eda7 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Fri, 31 May 2024 18:24:08 -0500 Subject: [PATCH 50/52] Shorten line length a little in RectKwargs --- arcade/types/rect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/types/rect.py b/arcade/types/rect.py index d7bffb782e..59f6946c75 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -15,7 +15,7 @@ class RectKwargs(TypedDict): - """Annotates an ordinary :py:class:`dict` of :py:class:`Rect` arguments. + """Annotates a plain :py:class:`dict` of :py:class:`Rect` arguments. This is only meaningful as a type annotation during type checking. For example, the :py:meth:`Rect.kwargs ` From 508dadb4010d6b17b1739f193e09a9529d4f4521 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Fri, 31 May 2024 19:52:13 -0500 Subject: [PATCH 51/52] Partial cleanup attempts for rect doc --- arcade/types/rect.py | 161 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 127 insertions(+), 34 deletions(-) diff --git a/arcade/types/rect.py b/arcade/types/rect.py index 59f6946c75..8d35dd8201 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -164,7 +164,7 @@ def resize(self, height: Optional[AsFloat] = None, anchor: Vec2 = AnchorPoint.CENTER) -> Rect: """ - Returns a new :py:class:`~arcade.types.rect.Rect` at the current Rect's position, + Returns a new :py:class:`Rect` at the current Rect's position, but with a new width and height, anchored at a point (default center.) """ width = width or self.width @@ -185,7 +185,7 @@ def resize(self, def scale(self, new_scale: AsFloat, anchor: Vec2 = AnchorPoint.CENTER) -> Rect: """ - Returns a new :py:class:`~arcade.types.rect.Rect` scaled by a factor of `new_scale`, + Returns a new :py:class:`Rect` scaled by a factor of ``new_scale``, anchored at a point (default center.) """ anchor_x = self.left + anchor.x * self.width @@ -200,7 +200,7 @@ def scale(self, new_scale: AsFloat, anchor: Vec2 = AnchorPoint.CENTER) -> Rect: def scale_axes(self, new_scale: Point2, anchor: Vec2 = AnchorPoint.CENTER) -> Rect: """ - Returns a new :py:class:`~arcade.types.rect.Rect` + Return a new :py:class:`Rect` scaled by a factor of `new_scale.x` in the width and `new_scale.y` in the height, anchored at a point (default center.) """ @@ -221,33 +221,33 @@ def __mul__(self, scale: AsFloat) -> Rect: # type: ignore[override] self.width * scale, self.height * scale, self.x * scale, self.y * scale) def __truediv__(self, scale: AsFloat) -> Rect: - """Scale the Rect by 1/``scale`` relative to ``(0, 0)``.""" + """Scale the rectangle by 1/``scale`` relative to ``(0, 0)``.""" return Rect(self.left / scale, self.right / scale, self.bottom / scale, self.top / scale, self.width / scale, self.height / scale, self.x / scale, self.y / scale) def align_top(self, value: AsFloat) -> Rect: - """Returns a new :py:class:`~arcade.types.rect.Rect`, which is aligned to the top at `value`.""" + """Returns a new :py:class:`Rect`, which is aligned to the top at `value`.""" return LBWH(self.left, value - self.height, self.width, self.height) def align_bottom(self, value: AsFloat) -> Rect: - """Returns a new :py:class:`~arcade.types.rect.Rect`, which is aligned to the bottom at `value`.""" + """Returns a new :py:class:`Rect`, which is aligned to the bottom at `value`.""" return LBWH(self.left, value, self.width, self.height) def align_left(self, value: AsFloat) -> Rect: - """Returns a new :py:class:`~arcade.types.rect.Rect`, which is aligned to the left at `value`.""" + """Returns a new :py:class:`Rect`, which is aligned to the left at `value`.""" return LBWH(value, self.bottom, self.width, self.height) def align_right(self, value: AsFloat) -> Rect: - """Returns a new :py:class:`~arcade.types.rect.Rect`, which is aligned to the right at `value`.""" + """Returns a new :py:class:`Rect`, which is aligned to the right at `value`.""" return LBWH(value - self.width, self.bottom, self.width, self.height) def align_center(self, value: Point2) -> Rect: - """Returns a new :py:class:`~arcade.types.rect.Rect`, which is aligned to the center x and y at `value`.""" + """Returns a new :py:class:`Rect`, which is aligned to the center x and y at `value`.""" cx, cy = value return XYWH(cx, cy, self.width, self.height) def align_x(self, value: AsFloat) -> Rect: - """Returns a new :py:class:`~arcade.types.rect.Rect`, which is aligned to the x at `value`.""" + """Returns a new :py:class:`Rect`, which is aligned to the x at `value`.""" return XYWH(value, self.y, self.width, self.height) @warning(ReplacementWarning, message=".align_center_x() is deprecated. Please use .align_x() instead.") @@ -256,7 +256,7 @@ def align_center_x(self, value: AsFloat) -> Rect: return self.align_x(value) def align_y(self, value: AsFloat) -> Rect: - """Returns a new :py:class:`~arcade.types.rect.Rect`, which is aligned to the y at `value`.""" + """Get a new :py:class:`Rect`, which is aligned to the y at `value`.""" return XYWH(self.x, value, self.width, self.height) @warning(ReplacementWarning, message=".align_center_y() is deprecated. Please use .align_y() instead.") @@ -325,20 +325,17 @@ def clamp_size(self, min_width: Optional[AsFloat] = None, max_width: Optional[AsFloat] = None, min_height: Optional[AsFloat] = None, max_height: Optional[AsFloat] = None, anchor: Vec2 = AnchorPoint.CENTER) -> Rect: - """ - Return a :py:class:`~arcade.types.rect.Rect` that is has a height between `min_height` and `max_height` and - a width between `min_width` and `max_width`, positioned at the current position and anchored to a point. - (default center) + """Get a new clamped-size rectangle at the same position and anchored at ``anchor_point``. + + This combines the effects of :py:meth:`clamp_width` and :py:meth:`clamp_height` into + one call. """ width = min(max_width or float("inf"), max(min_width or 0.0, self.width)) height = min(max_height or float("inf"), max(min_height or 0.0, self.height)) return self.resize(width, height, anchor) def union(self, other: Rect) -> Rect: - """ - Returns a new :py:class:`~arcade.types.rect.Rect` that is the union of this rect and another. - The union is the smallest rectangle that contains these two rectangles. - """ + """Get the smallest rectangle covering both this one and ``other``.""" left = min(self.left, other.left) right = max(self.right, other.right) bottom = min(self.bottom, other.bottom) @@ -346,13 +343,19 @@ def union(self, other: Rect) -> Rect: return LRBT(left, right, bottom, top) def __or__(self, other: Rect) -> Rect: - """Returns the result of :py:meth:`union` with ``other``.""" + """Shorthand for :py:meth:`rect.union(other) `. + + :param other: Another :py:class:`Rect` instance. + """ return self.union(other) def intersection(self, other: Rect) -> Rect | None: - """ - Returns a new :py:class:`~arcade.types.rect.Rect` that is the overlaping portion of this Rect and another. - This will return None if no such rectangle exists. + """Return a :py:class:`Rect` of the overlap if any exists. + + If the two :py:class:`Rect` instances do not intersect, this + method will return ``None`` instead. + + :param other: Another :py:class:`Rect` instance. """ intersecting = self.overlaps(other) if not intersecting: @@ -364,12 +367,16 @@ def intersection(self, other: Rect) -> Rect | None: return LRBT(left, right, bottom, top) def __and__(self, other: Rect) -> Rect | None: - """Returns the result of :py:meth:`intersection` with ``other``.""" + """Shorthand for :py:meth:`rect.intersection(other) `. + + :param other: Another :py:class:`Rect` instance. + """ return self.intersection(other) def overlaps(self, other: Rect) -> bool: - """ - Returns True if `other` overlaps with the rect. + """Returns ``True`` if `other` overlaps with ``self``. + + :param other: Another :py:class:`Rect` instance. """ return ( @@ -379,12 +386,18 @@ def overlaps(self, other: Rect) -> bool: ) def point_in_rect(self, point: Point2) -> bool: - """Returns True if the given point is inside this rectangle.""" + """Returns ``True`` if ``point`` is inside this rectangle. + + :param point: A tuple of :py:class:`int` or :py:class:`float` values. + """ px, py = point return (self.left < px < self.right) and (self.bottom < py < self.top) def __contains__(self, point: Point2) -> bool: - """Returns the result of :py:meth:`point_in_rect` with a provided point.""" + """Shorthand for :py:meth:`rect.point_in_rect(point) `. + + :param point: A tuple of :py:class:`int` or :py:class:`float` values. + """ return self.point_in_rect(point) def distance_from_bounds(self, point: Point2) -> float: @@ -397,12 +410,62 @@ def distance_from_bounds(self, point: Point2) -> float: return d def point_on_bounds(self, point: Point2, tolerance: float) -> bool: - """Returns True if the given point is on the bounds of this rectangle within some tolerance.""" + """Returns ``True`` if ``point`` is within ``tolerance`` of the bounds. + + The ``point``'s distance from the bounds is computed by through + :py:meth:`distance_from_bounds`. + + :param point: + """ return abs(self.distance_from_bounds(point)) < tolerance def position_to_uv(self, point: Point2) -> Vec2: - """Take an absolute point and translate it to it's relative position in UV-space - (percentage across this rectangle.) + """Convert a point to UV space values inside the rectangle. + + This is like a pair of ratios which measure how far the ``point`` + is from the rectangle's :py:meth:`bottom_left` up toward its + :py:meth:`top_right` along each axis. + + .. warning:: This method does not clamp output! + + Since ``point`` is absolute pixels, one or both axes + of the returned :py:class:`~pyglet.math.Vec2` can be: + + * less than ``0.0`` + * greater than ``1.0`` + + + Each axis of the return value measures how far into + the rectangle's ``size`` the ``point`` is relative + to the :py:meth:`bottom_left`: + + .. code-block:: python + + # consult the diagram below + Vec2( + (point.x - rect.left) / rect.width, + (point.y - rect.bottom) / rect.height + ) + + .. code-block:: + + |------- rect.width ------| + + The rectangle (rect.top_right) + +-------------------------T --- + | | | + - - - - - - - P (Point x, y)| | + | | | rect.height + | | | | | + y | | | + | B----------|--------------+ --- + | (rect.bottom_right) + | + O----- x -----| + + :param point: A point relative to the rectangle's + :py:meth:`bottom_left` corner. + """ x, y = point return Vec2( @@ -411,8 +474,29 @@ def position_to_uv(self, point: Point2) -> Vec2: ) def uv_to_position(self, uv: Point2) -> Vec2: - """Take a point in UV-space (percentage across this rectangle) and translate it - to it's absolute position. + """Convert a point in UV-space to a point within the rectangle. + + The ``uv`` is a pair of ratios which describe how far a point + extends across the rectangle's :py:attr:`width` and + :py:attr:`height` from the :py:attr:`bottom_left` toward its + :py:attr:`top_right`. + + .. warning:: This method does not clamp output! + + Since one or both of ``uv``'s components can be + less than ``0.0`` or greater than ``1.0``, the + returned point can fall outside the rectangle. + + The following can be used as arguments to this function: + + #. Values in :py:class:`~arcade.types.AnchorPoint` + #. Returned values from :py:meth:`position_to_uv` + #. Rescaled input data from controllers + + :param uv: A pair of ratio values describing how far a + a point falls from a rectangle's :py:attr:`bottom_left` + toward its :py:attr:`top_right`. + """ x, y = uv return Vec2( @@ -421,7 +505,16 @@ def uv_to_position(self, uv: Point2) -> Vec2: ) def to_points(self) -> tuple[Vec2, Vec2, Vec2, Vec2]: - """Returns a tuple of the four corners of this Rect.""" + """Return a new :py:class:`tuple` of this rectangle's corner points. + + The points will be ordered as follows: + + #. :py:meth:`bottom_left` + #. :py:meth:`top_left` + #. :py:meth:`top_right` + #. :py:meth:`bottom_right` + + """ left = self.left bottom = self.bottom right = self.right From 68b595c40d56e61fba45aec9e170ea3c18185cc9 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Fri, 31 May 2024 22:18:22 -0400 Subject: [PATCH 52/52] more minor doc cleanup --- arcade/types/rect.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/arcade/types/rect.py b/arcade/types/rect.py index 8d35dd8201..036af76191 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -148,14 +148,14 @@ def aspect_ratio(self) -> float: return self.width / self.height def at_position(self, position: Point2) -> Rect: - """Returns a new :py:class:`~arcade.types.rect.Rect` which is moved to put `position` at its center.""" + """Returns a new :py:class:`Rect` which is moved to put `position` at its center.""" x, y = position return XYWH(x, y, self.width, self.height) def move(self, dx: AsFloat = 0.0, dy: AsFloat = 0.0) -> Rect: """ - Returns a new :py:class:`~arcade.types.rect.Rect` - which is moved by `dx` in the x-direction and `dy` in the y-direction. + Returns a new :py:class:`Rect` which is moved by `dx` in the + x-direction and `dy` in the y-direction. """ return XYWH(self.x + dx, self.y + dy, self.width, self.height) @@ -271,7 +271,7 @@ def min_size( anchor: Vec2 = AnchorPoint.CENTER ) -> Rect: """ - Return a :py:class:`~arcade.types.rect.Rect` that is at least size `width` by `height`, positioned at + Return a :py:class:`Rect` that is at least size `width` by `height`, positioned at the current position and anchored to a point (default center.) """ width = max(width or 0.0, self.width) @@ -285,7 +285,7 @@ def max_size( anchor: Vec2 = AnchorPoint.CENTER ) -> Rect: """ - Return a :py:class:`~arcade.types.rect.Rect` that is at most size `width` by `height`, positioned at + Return a :py:class:`Rect` that is at most size `width` by `height`, positioned at the current position and anchored to a point (default center.) """ width = min(width or float("inf"), self.width) @@ -295,7 +295,7 @@ def max_size( def clamp_height(self, min_height: Optional[AsFloat] = None, max_height: Optional[AsFloat] = None, anchor: Vec2 = AnchorPoint.CENTER) -> Rect: """ - Return a :py:class:`~arcade.types.rect.Rect` that has a height between `min_height` and `max_height`, + Return a :py:class:`Rect` that has a height between `min_height` and `max_height`, positioned at the current position and anchored to a point (default center.) """ height = min(max_height or float("inf"), max(min_height or 0.0, self.height))