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', diff --git a/arcade/application.py b/arcade/application.py index 9d6871d874..c86b7efb61 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__) @@ -315,6 +316,11 @@ def background_color(self) -> Color: 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) + def run(self) -> None: """ Run the main loop. 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/camera/camera_2d.py b/arcade/camera/camera_2d.py index 26bce68325..d24325c4d7 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -11,8 +11,10 @@ ZeroProjectionDimension ) from arcade.gl import Framebuffer -from pyglet.math import Vec2 +from pyglet.math import Vec2, Vec3 +from arcade.types import Point, Rect, LBWH +from arcade.types.vector_like import Point2 from arcade.window_commands import get_window if TYPE_CHECKING: @@ -66,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): @@ -81,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=}" @@ -115,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 @@ -129,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. @@ -157,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. """ @@ -186,7 +207,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: @@ -195,7 +216,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 @@ -234,13 +257,14 @@ 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: - self._camera_data.position = (_pos[0], _pos[1], self._camera_data.position[2]) + def position(self, _pos: Point2) -> None: + x, y = _pos + self._camera_data.position = (x, y, self._camera_data.position[2]) # top_left @property @@ -252,18 +276,19 @@ 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: Tuple[float, float]): + def top_left(self, new_corner: Point2): up = self._camera_data.up 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 @@ -273,14 +298,15 @@ 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: Tuple[float, float]): + 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 @@ -292,18 +318,19 @@ 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: Tuple[float, float]): + def top_right(self, new_corner: Point2): up = self._camera_data.up 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 @@ -315,18 +342,19 @@ 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: Tuple[float, float]): + def bottom_right(self, new_corner: Point2): up = self._camera_data.up 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 @@ -337,14 +365,15 @@ 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: Tuple[float, float]): + 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 @@ -356,18 +385,19 @@ 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: Tuple[float, float]): + def bottom_left(self, new_corner: Point2): up = self._camera_data.up 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 @@ -377,13 +407,15 @@ 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: 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 + + x, y = new_right + self.position = x - up[1] * right, y + up[0] * right # center_left @property @@ -392,15 +424,17 @@ 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: 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: + x, y = new_left + self.position = x - up[1] * left, y - up[0] * left + + 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. """ @@ -418,7 +452,7 @@ def point_in_view(self, point: Tuple[float, float]) -> 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 @@ -434,32 +468,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: - raise ZeroProjectionDimension(( - f"projection width is 0 due to equal {left=}" - f"and {right=} values")) - if bottom == top: + if not value: 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: @@ -474,13 +498,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: @@ -495,13 +519,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: @@ -516,8 +540,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: @@ -532,8 +556,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: @@ -548,8 +572,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: @@ -564,8 +588,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: @@ -578,8 +602,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: @@ -592,22 +616,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) -> Optional[Rect]: + return self._ortho_projector.scissor + + @scissor.setter + def scissor(self, scissor: Rect): + self._ortho_projector.scissor = scissor @property def viewport_width(self) -> int: @@ -615,12 +647,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: @@ -628,73 +659,69 @@ 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) -> Tuple[float, float]: + def up(self) -> Vec2: """ A 2D vector which describes what is mapped to the +Y direction on screen. @@ -702,10 +729,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. @@ -715,7 +742,8 @@ def up(self, _up: Tuple[float, float]) -> 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: @@ -771,8 +799,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: """ @@ -782,7 +810,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() @@ -818,15 +846,13 @@ 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: 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 @@ -844,4 +870,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 0530742c60..c9896d0e51 100644 --- a/arcade/camera/data_types.py +++ b/arcade/camera/data_types.py @@ -4,12 +4,13 @@ wide usage throughout Arcade's camera code. """ from __future__ import annotations -from typing import Protocol, Tuple, Optional, Generator from contextlib import contextmanager +from typing import Protocol, Tuple, Generator from typing_extensions import Self -from pyglet.math import Vec3 +from pyglet.math import Vec2, Vec3 +from arcade.types import AsFloat, Point, Point3, Rect, LRBT __all__ = [ 'CameraData', @@ -36,7 +37,6 @@ class ZeroProjectionDimension(ValueError): ... - class CameraData: """Stores position, orientation, and zoom for a camera. @@ -54,9 +54,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 @@ -115,7 +115,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, @@ -124,31 +124,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. @@ -160,26 +248,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__() @@ -225,12 +308,13 @@ class Projection(Protocol): camera's :py:attr:`.CameraData.forward` vector. """ - viewport: Tuple[int, int, int, int] near: float far: float + class Projector(Protocol): + """Projects from world coordinates to viewport pixel coordinates. Projectors also support converting in the opposite direction from @@ -293,15 +377,13 @@ 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]: + 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 0c16d51e3d..fc404755e6 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -2,8 +2,9 @@ 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.window_commands import get_window if TYPE_CHECKING: from arcade.context import ArcadeContext @@ -76,31 +77,25 @@ 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] + x, y, *z = world_coordinate + return Vec2(x, y) def unproject( self, - screen_coordinate: Tuple[float, float], - depth: Optional[float] = None) -> Tuple[float, float, float]: + screen_coordinate: Point) -> 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 + x, y, *z = screen_coordinate + z = 0.0 if not z else z[0] - def map_screen_to_world_coordinate( - self, - screen_coordinate: Tuple[float, float], - depth: Optional[float] = None) -> Tuple[float, float, float]: - """ - 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 07e239710d..51e91e65e2 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,7 @@ unproject_orthographic ) +from arcade.types import Point, Rect, LBWH from arcade.window_commands import get_window if TYPE_CHECKING: from arcade import Window @@ -49,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 @@ -63,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 @@ -105,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 @@ -134,29 +140,19 @@ 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 """ - 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.viewport.viewport, _view, _projection, ) - return pos.x, pos.y - - 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 @@ -171,14 +167,11 @@ 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 + self.viewport.viewport, + _view, _projection ) - - return pos.x, pos.y, pos.z diff --git a/arcade/camera/perspective.py b/arcade/camera/perspective.py index 3f7f33910d..e0a4371c06 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 ( @@ -13,6 +13,7 @@ unproject_perspective ) +from arcade.types import Point, Rect, LBWH from arcade.window_commands import get_window if TYPE_CHECKING: from arcade import Window @@ -49,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 @@ -62,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 @@ -129,35 +134,31 @@ 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 - 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 """ - 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.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 = project_perspective( Vec3(x, y, z), - self._projection.viewport, + self.viewport.viewport, _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]: + def unproject(self, screen_coordinate: Point) -> Vec3: """ Take in a pixel coordinate from within the range of the window size and returns @@ -165,31 +166,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.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( - screen_coordinate, self.projection.viewport, - _view, _projection, - depth + Vec3(x, y, z), self.viewport.viewport, + _view, _projection ) - return pos.x, pos.y, pos.z - - def map_screen_to_world_coordinate( - self, - screen_coordinate: Tuple[float, float], - depth: Optional[float] = None) -> Tuple[float, float, float]: - """ - Alias of PerspectiveProjector.unproject() for typing. - """ - return self.unproject(screen_coordinate, depth) + return pos diff --git a/arcade/camera/projection_functions.py b/arcade/camera/projection_functions.py index 9874075f4a..857d0a4236 100644 --- a/arcade/camera/projection_functions.py +++ b/arcade/camera/projection_functions.py @@ -1,8 +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 import Point def generate_view_matrix(camera_data: CameraData) -> Mat4: @@ -24,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. @@ -62,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. @@ -95,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) @@ -114,10 +112,13 @@ 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: 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 @@ -125,15 +126,18 @@ def unproject_orthographic(screen_coordinate: Union[Vec2, Tuple[float, float]], _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: Point, 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 + z = 1.0 if not z else z[0] + + world_position = Vec4(x, y, z, 1.0) semi_projected_position = projection_matrix @ view_matrix @ world_position div_val = semi_projected_position.w @@ -147,21 +151,22 @@ 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: 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 9abded185e..13d21759dc 100644 --- a/arcade/camera/static.py +++ b/arcade/camera/static.py @@ -14,6 +14,7 @@ unproject_perspective ) +from arcade.types import Point, Point3 from arcade.window_commands import get_window from pyglet.math import Mat4, Vec3, Vec2 @@ -27,18 +28,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 @@ -56,7 +55,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 """ @@ -64,14 +63,12 @@ def project(self, world_coordinate: Tuple[float, ...]) -> Tuple[float, float]: 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.x, pos.y + return pos - 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 @@ -82,29 +79,29 @@ 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.x, pos.y, pos.z + 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 ) @@ -113,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 ) @@ -129,9 +127,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 @@ -141,7 +139,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, @@ -152,9 +150,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 @@ -163,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, @@ -175,9 +173,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[[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) diff --git a/arcade/draw_commands.py b/arcade/draw_commands.py index d0937bc187..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 +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 @@ -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/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/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/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/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/nine_patch.py b/arcade/gui/nine_patch.py index 1a5e152c9c..e60da684f2 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,15 @@ 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(int(rect.left), int(rect.right), int(rect.bottom), int(rect.top), texture, atlas=atlas) + @property def ctx(self) -> arcade.ArcadeContext: """The OpenGL context.""" diff --git a/arcade/gui/surface.py b/arcade/gui/surface.py index 2c42e26ded..26bb4c52de 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, LBWH class Surface: @@ -56,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 @@ -166,15 +168,14 @@ 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() 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"] = (0, 0, *self._size) if not area else area.lbwh self._geometry.render(self._program, vertices=1) # Restore blend function diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index 1522dd537f..5e43f39fac 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) @@ -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 @@ -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, 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 cd43a68038..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, PointList, EMPTY_POINT_LIST +from arcade.types import 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,8 +102,8 @@ class HitBox: """ def __init__( self, - points: PointList, - position: Point = (0.0, 0.0), + points: Point2List, + position: Point2 = (0.0, 0.0), scale: Tuple[float, float] = (1.0, 1.0), ): self._points = points @@ -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/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/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 """ diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 26a45e270f..c6f82c1fc0 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -3,12 +3,14 @@ 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.utils import copy_dunders_unimplemented +from pyglet.math import Vec2 + if TYPE_CHECKING: from arcade.sprite_list import SpriteList @@ -16,7 +18,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. @@ -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"] = [] @@ -75,7 +77,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. @@ -85,7 +87,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 @@ -201,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] @@ -212,16 +214,17 @@ 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 - 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] @@ -296,6 +299,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. @@ -360,7 +367,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")) @@ -572,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] @@ -646,7 +653,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/colored.py b/arcade/sprite/colored.py index c52b7d38d3..021438da48 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,11 @@ def __init__( ) self.color = Color.from_iterable(color) + @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 cls(int(rect.width), int(rect.height), rect.x, rect.y, color, angle) + class SpriteCircle(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/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/text.py b/arcade/text.py index 900ee45a71..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__ = [ @@ -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 d8e8b51ddd..43fdc8f730 100644 --- a/arcade/texture/loading.py +++ b/arcade/texture/loading.py @@ -8,7 +8,6 @@ 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 @@ -178,7 +177,7 @@ def load_texture_pair( def load_textures( file_name: Union[str, Path], - image_location_list: RectList, + 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/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/texture/texture.py b/arcade/texture/texture.py index cf2d50c7ec..a6fef893f9 100644 --- a/arcade/texture/texture.py +++ b/arcade/texture/texture.py @@ -26,7 +26,8 @@ TransverseTransform, ) -from arcade.types import RGBA255, PointList +from arcade.types import RGBA255, Point2List +from arcade.types.rect import Rect if TYPE_CHECKING: from arcade import TextureAtlas @@ -154,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, ): @@ -191,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() ) @@ -392,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. @@ -753,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 @@ -842,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 @@ -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 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/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/tilemap/tilemap.py b/arcade/tilemap/tilemap.py index cd917fe2b7..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, IntRect, 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], IntRect, 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 309ecadc5a..3c56bde7ff 100644 --- a/arcade/types/__init__.py +++ b/arcade/types/__init__.py @@ -26,10 +26,8 @@ import sys from pathlib import Path from typing import ( - List, NamedTuple, Optional, - Sequence, Tuple, Union, TYPE_CHECKING, @@ -76,7 +74,14 @@ # The Color helper type from arcade.types.color import Color -# We'll be moving our Vec-like items into this (Points, Sizes, etc) +# 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 @@ -86,6 +91,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 @@ -99,20 +105,22 @@ "PathOr", "PathOrTexture", "Point", + "Point2", "Point3", "PointList", + "Point2List", + "Point3List", "EMPTY_POINT_LIST", "AnchorPoint", - "IntRect", "Rect", "LRBT", + "LBWH", "XYWH", "XYRR", "Viewport", "ViewportParams", "RectParams", "RectKwargs", - "RectList", "RGB", "RGBA", "RGBOrA", @@ -130,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: #: @@ -149,25 +159,14 @@ #: Size2D = Tuple[_T, _T] -# Point = Union[Tuple[AsFloat, AsFloat], List[AsFloat]] -Point = Tuple[AsFloat, AsFloat] -Point3 = Tuple[AsFloat, AsFloat, AsFloat] +#: 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] -PointList = Sequence[Point] -# Speed / typing workaround: -# 1. Eliminate extra allocations -# 2. Allows type annotation to be cleaner, primarily for HitBox & subclasses -EMPTY_POINT_LIST: PointList = tuple() - - -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 +# --- End potentially obsolete annotations --- # Path handling @@ -185,7 +184,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 diff --git a/arcade/types/rect.py b/arcade/types/rect.py index b860c7099d..036af76191 100644 --- a/arcade/types/rect.py +++ b/arcade/types/rect.py @@ -1,11 +1,12 @@ """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 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 @@ -14,6 +15,19 @@ class RectKwargs(TypedDict): + """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 ` + 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: float right: float bottom: float @@ -25,7 +39,7 @@ class RectKwargs(TypedDict): class Rect(NamedTuple): - """Rects 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. @@ -35,10 +49,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. @@ -124,27 +137,39 @@ 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.""" return self.width / self.height - def at_position(self, position: Vec2) -> 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) + def at_position(self, position: Point2) -> Rect: + """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) - 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, + 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 + height = height or self.height + anchor_x = self.left + anchor.x * self.width anchor_y = self.bottom + anchor.y * self.height @@ -160,7 +185,7 @@ def resize(self, width: AsFloat, height: AsFloat, anchor: Vec2 = AnchorPoint.CEN 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 @@ -173,44 +198,56 @@ 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` + 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.) """ 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) + 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) + + def __truediv__(self, scale: AsFloat) -> Rect: + """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: Vec2) -> 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) + def align_center(self, value: Point2) -> Rect: + """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.") @@ -219,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.") @@ -234,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) @@ -248,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) @@ -258,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)) @@ -288,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) @@ -309,12 +343,19 @@ def union(self, other: Rect) -> Rect: return LRBT(left, right, bottom, top) def __or__(self, other: Rect) -> Rect: + """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: @@ -326,11 +367,16 @@ def intersection(self, other: Rect) -> Rect | None: return LRBT(left, right, bottom, top) def __and__(self, other: Rect) -> Rect | None: + """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 ( @@ -339,21 +385,136 @@ 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: - """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_in_rect(self, point: Point2) -> bool: + """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: + """Shorthand for :py:meth:`rect.point_in_rect(point) `. - 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.""" - diff = Vec2(point.x - self.x, point.y - self.y) + :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: + """Returns the point's distance from the boundary of this rectangle.""" + 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: Point2, tolerance: float) -> bool: + """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: + """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 + ) - return abs(d) < tolerance + .. 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( + (x - self.left) / self.width, + (y - self.bottom) / self.height, + ) + + def uv_to_position(self, uv: Point2) -> Vec2: + """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( + 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.""" + """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 @@ -387,17 +548,18 @@ 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, 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: """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} @@ -420,6 +582,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, @@ -436,11 +635,42 @@ 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 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) @@ -449,7 +679,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) @@ -458,7 +688,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 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 @@ -470,7 +703,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 @@ -481,7 +714,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) diff --git a/arcade/types/vector_like.py b/arcade/types/vector_like.py index b148383c1d..0ae5ef8429 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,12 +8,60 @@ """ from __future__ import annotations +from typing import Union, Tuple, Sequence -from pyglet.math import Vec2 +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: - """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 + shaders. + """ + + BOTTOM_LEFT = Vec2(0.0, 0.0) BOTTOM_CENTER = Vec2(0.5, 0.0) BOTTOM_RIGHT = Vec2(1.0, 0.0) @@ -26,5 +74,12 @@ class AnchorPoint: __all__ = [ - 'AnchorPoint' + 'Point2', + 'Point3', + 'Point', + 'Point2List', + 'Point3List', + 'PointList', + 'AnchorPoint', + 'EMPTY_POINT_LIST' ] 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_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..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) @@ -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)]) @@ -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 @@ -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)) ) @@ -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 @@ -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/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): diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index 9e33b27c4a..0588c06157 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 @@ -48,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 @@ -259,6 +260,7 @@ def update(delta_time): frame = 0 + def test_sprite_removal(window): CHARACTER_SCALING = 0.5 global frame @@ -282,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() @@ -344,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) @@ -391,24 +392,24 @@ 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 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) @@ -438,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 @@ -446,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 @@ -454,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 @@ -462,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 @@ -531,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 @@ -550,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 @@ -561,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 @@ -572,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 @@ -637,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 @@ -651,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 @@ -665,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 @@ -699,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 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}