From 13cc398757ed524ae072b6df34aa4be493925035 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 1 Dec 2025 20:46:16 +1300 Subject: [PATCH 01/17] make camera2D position setter use `pos` not `_pos` and add x, y properties --- arcade/camera/camera_2d.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 52e8096aa..94882b572 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -2,7 +2,7 @@ from collections.abc import Generator from contextlib import contextmanager -from math import atan2, cos, degrees, radians, sin +from math import atan2, cos, degrees, radians, sin, pow from typing import TYPE_CHECKING from pyglet.math import Vec2, Vec3 @@ -553,11 +553,31 @@ def position(self) -> Vec2: # Setter with different signature will cause mypy issues # https://github.com/python/mypy/issues/3004 @position.setter - def position(self, _pos: Point) -> None: - x, y, *_z = _pos + def position(self, pos: Point) -> None: + x, y, *_z = pos z = self._camera_data.position[2] if not _z else _z[0] self._camera_data.position = (x, y, z) + @property + def x(self) -> float: + """The 2D world position of the camera along the X axis""" + return self._camera_data.position[0] + + @x.setter + def x(self, x: float) -> None: + pos = self._camera_data.position + self._camera_data.position = (x, pos[1], pos[2]) + + @property + def y(self) -> float: + """The 2D world position of the camera along the Y axis""" + return self._camera_data.position[1] + + @y.setter + def y(self, y: float) -> None: + pos = self._camera_data.position + self._camera_data.position = (pos[0], y, pos[2]) + @property def projection(self) -> Rect: """Get/set the left, right, bottom, and top projection values. From 9ea1320e943e77d6857344003e6c14d698f46ff2 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 1 Dec 2025 20:47:35 +1300 Subject: [PATCH 02/17] Add `move_to` method with optional duration as requested by Eruvanos --- arcade/camera/camera_2d.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 94882b572..14ed094dd 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -512,6 +512,39 @@ def point_in_view(self, point: Point2) -> bool: return abs(dot_x) <= h_width and abs(dot_y) <= h_height + def move_to(self, position: Point2, *, duration: float | None = None) -> Point2: + """ + Move the camera to the provided position. + If duration is None is the same as setting camera.position. + Rate makes it easy to move the camera smoothly over time. + + When duration is not None it uses `arcade.math.smerp` method to + smoothly move to the target position. This means duration does + NOT equal the fraction to move. To make the motion frame rate + independant use `duration = dt * T` where T is the number + of seconds to move half the distance to the target position. + + Args: + position: x, y position in world space to move too + duration: The number of frames it takes to approximately move half-way + to the target position + + Returns: + The actual position the camera was set too. + """ + if duration is None: + x, y = position + self._camera_data.position = (x, y, self._camera_data.position[2]) + return position + + x1, y1, z1 = self._camera_data.position + x2, y2 = position + d = pow(2, -duration) + x = x1 + (x2 - x1) * d + y = y1 + (y2 - y1) * d + + self._camera_data.position = (x, y, z1) + return x, y @property def view_data(self) -> CameraData: """The view data for the camera. From 2d292cdd4c1af185d23a6915efa549f9ea5a1df6 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 1 Dec 2025 21:03:23 +1300 Subject: [PATCH 03/17] add `Camera2D.move_by` method to avoid position access costs --- arcade/camera/camera_2d.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 14ed094dd..cb302d815 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -545,6 +545,24 @@ def move_to(self, position: Point2, *, duration: float | None = None) -> Point2: self._camera_data.position = (x, y, z1) return x, y + + def move_by(self, change: Point2) -> Point2: + """ + Move the camera in world space along the XY axes by the provided change. + If you want to drag the camera with a mouse `camera2D.drag_by` is the method + to use. + + Args: + change: amount to move XY position in world space + + Returns: + final XY position of the camera + """ + pos = self._camera_data.position + new = pos[0] + change[0], pos[1] + change[1] + self._camera_data.position = new[0], new[1], pos[2] + return new + @property def view_data(self) -> CameraData: """The view data for the camera. From afcf5b21ef385cd54c0b2857dd6d237d7c85c8a7 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 1 Dec 2025 21:04:17 +1300 Subject: [PATCH 04/17] add `Camera2D.drag_by` method to allow for accurate dragging --- arcade/camera/camera_2d.py | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index cb302d815..b094fc027 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -563,6 +563,51 @@ def move_by(self, change: Point2) -> Point2: self._camera_data.position = new[0], new[1], pos[2] return new + def drag_by(self, change: Point2) -> Point2: + """ + Move the camera in world space by an amount in screen space. + This is a utility method to make it easy to drag the camera correctly. + normally zooming in/out, rotating the camera, and using a non 1:1 projection + causes the mouse dragging to desync with the camera motion. It automatically + negates the change so the change represents the amount the camera `appears` + to move. This is because moving the camera left makes everything appear to + move right. So a user moving the mouse right wants expects the camera to move + left. + + The simplest use case is with the Window/View's `on_mouse_drag` + ```python + def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + self.camera.drag_by((dx, dy)) + ``` + + ! This method is more expensive than `Camera2D.move_by` so use only when needed. + ! If your camera is 1:1 with the screen and you only zoom in and out you can get + ! away with `camera2D.move_by(-change / camera.zoom)`. + + ! This method must assume that viewport has the same pixel scale as the + ! window. If you are doing some form of upscaling you will have to scale + ! the mouse dx and dy by the difference in pixel scale. + + Args: + change: The number of pixels to move the camera by + + Returns: + The final position of the camera. + """ + + # Early exit to avoid expensive matrix generation + if change[0] == 0.0 and change[1] == 0.0: + return self._camera_data.position[0], self._camera_data.position[1] + + x0, y0, _ = self.unproject((0, 0)) + xc, yc, _ = self.unproject(change) + + dx, dy = xc - x0, yc - y0 + pos = self._camera_data.position + new = pos[0] - dx, pos[1] - dy + self._camera_data.position = new[0], new[1], pos[2] + return new + @property def view_data(self) -> CameraData: """The view data for the camera. From dcf4a96954b6a8d90ac58ba03c2c8d996d7c6dc0 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 1 Dec 2025 21:10:20 +1300 Subject: [PATCH 05/17] linting and formatting passs --- arcade/camera/camera_2d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index b094fc027..1da5f8580 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -2,7 +2,7 @@ from collections.abc import Generator from contextlib import contextmanager -from math import atan2, cos, degrees, radians, sin, pow +from math import atan2, cos, degrees, pow, radians, sin from typing import TYPE_CHECKING from pyglet.math import Vec2, Vec3 From 258b9ad8e5fd85b0b9406ac5e6311abef64bea88 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 1 Dec 2025 23:25:11 +1300 Subject: [PATCH 06/17] Sphyinxify docs --- arcade/camera/camera_2d.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 1da5f8580..f45525df0 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -515,14 +515,14 @@ def point_in_view(self, point: Point2) -> bool: def move_to(self, position: Point2, *, duration: float | None = None) -> Point2: """ Move the camera to the provided position. - If duration is None is the same as setting camera.position. - Rate makes it easy to move the camera smoothly over time. + If duration is None this is the same as setting camera.position. + duration makes it easy to move the camera smoothly over time. - When duration is not None it uses `arcade.math.smerp` method to - smoothly move to the target position. This means duration does - NOT equal the fraction to move. To make the motion frame rate - independant use `duration = dt * T` where T is the number - of seconds to move half the distance to the target position. + When duration is not None it uses :py:func:`arcade.math.smerp` method + to smoothly move to the target position. This means duration does NOT + equal the fraction to move. To make the motion frame rate independant + use ``duration = dt * T`` where ``T`` is the number of seconds to move + half the distance to the target position. Args: position: x, y position in world space to move too @@ -549,8 +549,8 @@ def move_to(self, position: Point2, *, duration: float | None = None) -> Point2: def move_by(self, change: Point2) -> Point2: """ Move the camera in world space along the XY axes by the provided change. - If you want to drag the camera with a mouse `camera2D.drag_by` is the method - to use. + If you want to drag the camera with a mouse :py:func:`camera2D.drag_by` + is the method to use. Args: change: amount to move XY position in world space @@ -569,24 +569,25 @@ def drag_by(self, change: Point2) -> Point2: This is a utility method to make it easy to drag the camera correctly. normally zooming in/out, rotating the camera, and using a non 1:1 projection causes the mouse dragging to desync with the camera motion. It automatically - negates the change so the change represents the amount the camera `appears` + negates the change so the change represents the amount the camera appears to move. This is because moving the camera left makes everything appear to - move right. So a user moving the mouse right wants expects the camera to move + move right. So a user moving the mouse right expects the camera to move left. - The simplest use case is with the Window/View's `on_mouse_drag` + The simplest use case is with the Window/View's :py:func:`on_mouse_drag` ```python def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): self.camera.drag_by((dx, dy)) ``` - ! This method is more expensive than `Camera2D.move_by` so use only when needed. - ! If your camera is 1:1 with the screen and you only zoom in and out you can get - ! away with `camera2D.move_by(-change / camera.zoom)`. + .. warning:: This method is more expensive than :py:func:`Camera2D.move_by` so + use only when needed. If your camera is 1:1 with the screen and you + only zoom in and out you can get away with + ``camera2D.move_by(-change / camera.zoom)``. - ! This method must assume that viewport has the same pixel scale as the - ! window. If you are doing some form of upscaling you will have to scale - ! the mouse dx and dy by the difference in pixel scale. + .. warning:: This method must assume that viewport has the same pixel scale as the + window. If you are doing some form of upscaling you will have to scale + the mouse dx and dy by the difference in pixel scale. Args: change: The number of pixels to move the camera by From feebf7c7061bcf9232bf13ad247f4821560595c3 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 1 Dec 2025 23:36:18 +1300 Subject: [PATCH 07/17] code block in correct format --- arcade/camera/camera_2d.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index f45525df0..23f97eb7c 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -575,10 +575,10 @@ def drag_by(self, change: Point2) -> Point2: left. The simplest use case is with the Window/View's :py:func:`on_mouse_drag` - ```python - def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): - self.camera.drag_by((dx, dy)) - ``` + .. code-block:: python + + def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + self.camera.drag_by((dx, dy)) .. warning:: This method is more expensive than :py:func:`Camera2D.move_by` so use only when needed. If your camera is 1:1 with the screen and you From 3ae2e0bc0e80f4daf122f42d9a16440c19cc48bd Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 1 Dec 2025 23:40:38 +1300 Subject: [PATCH 08/17] better position doc string as part of #2558 --- arcade/camera/camera_2d.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 23f97eb7c..099a5da16 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -644,7 +644,13 @@ def projection_data(self) -> OrthographicProjectionData: @property def position(self) -> Vec2: - """The 2D world position of the camera along the X and Y axes.""" + """ + The 2D position of the camera. + + This is in world space, so the same as :py:class:`Sprite` and draw commands. + The default projection is a :py:func:`XYWH` rect positioned at (0, 0) so the + position of the camera is the center of the screen. + """ return Vec2(self._camera_data.position[0], self._camera_data.position[1]) # Setter with different signature will cause mypy issues From 7a327bece64436221c3bdd353816108512d9bdfc Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 1 Dec 2025 23:43:30 +1300 Subject: [PATCH 09/17] formatting pass for new docstring --- arcade/camera/camera_2d.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 099a5da16..911c250d6 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -645,11 +645,11 @@ def projection_data(self) -> OrthographicProjectionData: @property def position(self) -> Vec2: """ - The 2D position of the camera. + The 2D position of the camera. - This is in world space, so the same as :py:class:`Sprite` and draw commands. - The default projection is a :py:func:`XYWH` rect positioned at (0, 0) so the - position of the camera is the center of the screen. + This is in world space, so the same as :py:class:`Sprite` and draw commands. + The default projection is a :py:func:`XYWH` rect positioned at (0, 0) so the + position of the camera is the center of the screen. """ return Vec2(self._camera_data.position[0], self._camera_data.position[1]) From bc04c036ac663459937f06fbde2b23139959a6c1 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 1 Dec 2025 23:46:02 +1300 Subject: [PATCH 10/17] arcade uses american spelling much to my dismay --- arcade/camera/camera_2d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 911c250d6..85f14b028 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -322,7 +322,7 @@ def unproject(self, screen_coordinate: Point) -> Vec3: _view = generate_view_matrix(self.view_data) return unproject_orthographic(screen_coordinate, self.viewport.lbwh_int, _view, _projection) - def equalise(self) -> None: + def equalize(self) -> None: """ Forces the projection to match the size of the viewport. When matching the projection to the viewport the method keeps From 7ae4a01e2087b1e9e7947bc57de6b17cde728ede Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 1 Dec 2025 23:46:42 +1300 Subject: [PATCH 11/17] also remove alias of British spelling --- arcade/camera/camera_2d.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 85f14b028..256ffd061 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -331,8 +331,6 @@ def equalize(self) -> None: x, y = self._projection_data.rect.x, self._projection_data.rect.y self._projection_data.rect = XYWH(x, y, self.viewport_width, self.viewport_height) - equalize = equalise - def match_window( self, viewport: bool = True, From 39658475949ec075493d3696244a6a637cbf449e Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 2 Dec 2025 00:12:05 +1300 Subject: [PATCH 12/17] improve camera init position logic and add aspect argument to init --- arcade/camera/camera_2d.py | 46 ++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 256ffd061..b10cb542b 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -60,7 +60,11 @@ class Camera2D: If the viewport is not 1:1 with the projection then positions in world space won't match pixels on screen. position: - The 2D position of the camera in the XY plane. + The 2D position of the camera. + + This is in world space, so the same as :py:class:`Sprite` and draw commands. + The default projection is a :py:func:`XYWH` rect positioned at (0, 0) so the + position of the camera is the center of the viewport. up: A 2D vector which describes which direction is up (defines the +Y-axis of the camera space). @@ -75,6 +79,11 @@ class Camera2D: The near clipping plane of the camera. far: The far clipping plane of the camera. + aspect: The ratio between width and height that the viewport should + be constrained to. If unset then the viewport just matches the given + size. The aspect ratio describes how much larger the width should be + compared to the height. i.e. for an aspect ratio of ``4:3`` you should + input ``4.0/3.0`` or ``1.33333...``. Cannot be equal to zero. scissor: A ``Rect`` which will crop the camera's output to this area on screen. Unlike the viewport this has no influence on the visuals rendered with @@ -96,6 +105,7 @@ def __init__( near: float = DEFAULT_NEAR_ORTHO, far: float = DEFAULT_FAR, *, + aspect: float | None = None, scissor: Rect | None = None, render_target: Framebuffer | None = None, window: Window | None = None, @@ -111,7 +121,19 @@ def __init__( # but we need to have some form of default size. render_target = render_target or self._window.ctx.screen viewport = viewport or LBWH(*render_target.viewport) - width, height = viewport.size + + if aspect is None: + width, height = viewport.size + elif aspect == 0.0: + raise ZeroProjectionDimension( + "aspect ratio is 0 which will cause invalid viewport dimensions." + ) + elif viewport.height * aspect < viewport.width: + width = viewport.height * aspect + height = viewport.height + else: + width = viewport.width + height = viewport.width / aspect half_width = width / 2 half_height = height / 2 @@ -136,8 +158,10 @@ def __init__( f"projection depth is 0 due to equal {near=} and {far=} values" ) - pos_x = position[0] if position is not None else half_width - pos_y = position[1] if position is not None else half_height + # By using -left and -bottom this ensures that (0.0, 0.0) is always + # in the bottom left corner of the viewport + pos_x = position[0] if position is not None else -left + pos_y = position[1] if position is not None else -bottom self._camera_data = CameraData( position=(pos_x, pos_y, 0.0), up=(up[0], up[1], 0.0), @@ -350,7 +374,7 @@ def match_window( scissor: Flag whether to also equalize the scissor box to the viewport. On by default position: Flag whether to position the camera so that (0.0, 0.0) is in - the bottom-left + the bottom-left of the viewport aspect: The ratio between width and height that the viewport should be constrained to. If unset then the viewport just matches the window size. The aspect ratio describes how much larger the width should be @@ -384,7 +408,7 @@ def match_target( The projection center stays fixed, and the new projection matches only in size. scissor: Flag whether to update the scissor value. position: Flag whether to position the camera so that (0.0, 0.0) is in - the bottom-left + the bottom-left of the viewport aspect: The ratio between width and height that the value should be constrained to. i.e. for an aspect ratio of ``4:3`` you should input ``4.0/3.0`` or ``1.33333...``. Cannot be equal to zero. @@ -426,7 +450,7 @@ def update_values( The projection center stays fixed, and the new projection matches only in size. scissor: Flag whether to update the scissor value. position: Flag whether to position the camera so that (0.0, 0.0) is in - the bottom-left + the bottom-left of the viewport aspect: The ratio between width and height that the value should be constrained to. i.e. for an aspect ratio of ``4:3`` you should input ``4.0/3.0`` or ``1.33333...``. Cannot be equal to zero. @@ -452,7 +476,11 @@ def update_values( self.scissor = value if position: - self.position = Vec2(-self._projection_data.left, -self._projection_data.bottom) + self._camera_data.position = ( + -self._projection_data.left, + -self._projection_data.bottom, + self._camera_data.position[2] + ) def aabb(self) -> Rect: """ @@ -647,7 +675,7 @@ def position(self) -> Vec2: This is in world space, so the same as :py:class:`Sprite` and draw commands. The default projection is a :py:func:`XYWH` rect positioned at (0, 0) so the - position of the camera is the center of the screen. + position of the camera is the center of the viewport. """ return Vec2(self._camera_data.position[0], self._camera_data.position[1]) From cdcb76826908808e2c50a22eea20ef947ef04269 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 2 Dec 2025 00:16:43 +1300 Subject: [PATCH 13/17] formatting pass --- arcade/camera/camera_2d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index b10cb542b..de3320be1 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -479,7 +479,7 @@ def update_values( self._camera_data.position = ( -self._projection_data.left, -self._projection_data.bottom, - self._camera_data.position[2] + self._camera_data.position[2], ) def aabb(self) -> Rect: From 7c442ea44f34b89597033ac0444b7c8af9bba2ed Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 2 Dec 2025 00:28:33 +1300 Subject: [PATCH 14/17] additionally exception when aspect == 0 in update values --- arcade/camera/camera_2d.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index de3320be1..0e0f8e510 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -457,7 +457,11 @@ def update_values( If unset then the value will not be updated. """ if aspect is not None: - if value.height * aspect < value.width: + if aspect == 0.0: + raise ZeroProjectionDimension( + "aspect ratio is 0 which will cause invalid viewport dimensions." + ) + elif value.height * aspect < value.width: w = value.height * aspect h = value.height else: From d370cbd436a3d4aee208c2ebbeb70e3d1639fa75 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 2 Dec 2025 00:34:17 +1300 Subject: [PATCH 15/17] aspect ratio unit tests --- tests/unit/camera/test_camera2d.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/unit/camera/test_camera2d.py b/tests/unit/camera/test_camera2d.py index 076040507..df2922fb2 100644 --- a/tests/unit/camera/test_camera2d.py +++ b/tests/unit/camera/test_camera2d.py @@ -82,6 +82,25 @@ def test_camera2d_init_inheritance_safety(window: Window, camera_class): assert isinstance(subclassed, Camera2DSub1) +ASPECT_RATIOS = ( + 1.0, + 4.0/3.0, + 16.0/9.0, + 16.0/10.0 +) + + +def test_camera2d_init_aspect_equal_0_raises_zeroprojectiondimension(window: Window): + with pytest.raises(ZeroProjectionDimension): + camera = Camera2D(aspect=0.0) + + +@pytest.mark.parametrize("aspect", ASPECT_RATIOS) +def test_camera2d_init_respects_aspect_ratio(window: Window, aspect): + ortho_camera = Camera2D(aspect=aspect) + assert ortho_camera.viewport_width / ortho_camera.viewport_height == pytest.approx(aspect) + + RENDER_TARGET_SIZES = [ (800, 600), # Normal window size (1280, 720), # Bigger @@ -105,6 +124,9 @@ def test_camera2d_init_uses_render_target_size(window: Window, width, height): assert ortho_camera.viewport_bottom == 0 assert ortho_camera.viewport_top == height + assert ortho_camera.position.x == width/2.0 + assert ortho_camera.position.y == height/2.0 + @pytest.mark.parametrize("width, height", RENDER_TARGET_SIZES) def test_camera2d_from_camera_data_uses_render_target_size(window: Window, width, height): @@ -122,6 +144,9 @@ def test_camera2d_from_camera_data_uses_render_target_size(window: Window, width assert ortho_camera.viewport_bottom == 0 assert ortho_camera.viewport_top == height + assert ortho_camera.position.x == width/2.0 + assert ortho_camera.position.y == height/2.0 + def test_move_camera_and_project(window: Window): camera = Camera2D() From c563f2bd8a01f530538ff6741c36c31118a5a07b Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 2 Dec 2025 00:39:09 +1300 Subject: [PATCH 16/17] fix viewport not respecting aspect ratio and redundant rect reaction found this issue with unit tests hurra --- arcade/camera/camera_2d.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 0e0f8e510..d337c4e94 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -134,6 +134,7 @@ def __init__( else: width = viewport.width height = viewport.width / aspect + viewport = XYWH(viewport.x, viewport.y, width, height) half_width = width / 2 half_height = height / 2 @@ -172,7 +173,7 @@ def __init__( left=left, right=right, top=top, bottom=bottom, near=near, far=far ) - self.viewport: Rect = viewport or LRBT(0, 0, width, height) + self.viewport: Rect = viewport """ A rect which describes how the final projection should be mapped from unit-space. defaults to the size of the render_target or window From 792d43ec3187af1c228c0c6e528aed46a41d7659 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 2 Dec 2025 00:40:08 +1300 Subject: [PATCH 17/17] unit test formating --- tests/unit/camera/test_camera2d.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/unit/camera/test_camera2d.py b/tests/unit/camera/test_camera2d.py index df2922fb2..5ab9ad752 100644 --- a/tests/unit/camera/test_camera2d.py +++ b/tests/unit/camera/test_camera2d.py @@ -82,12 +82,7 @@ def test_camera2d_init_inheritance_safety(window: Window, camera_class): assert isinstance(subclassed, Camera2DSub1) -ASPECT_RATIOS = ( - 1.0, - 4.0/3.0, - 16.0/9.0, - 16.0/10.0 -) +ASPECT_RATIOS = (1.0, 4.0 / 3.0, 16.0 / 9.0, 16.0 / 10.0) def test_camera2d_init_aspect_equal_0_raises_zeroprojectiondimension(window: Window): @@ -124,8 +119,8 @@ def test_camera2d_init_uses_render_target_size(window: Window, width, height): assert ortho_camera.viewport_bottom == 0 assert ortho_camera.viewport_top == height - assert ortho_camera.position.x == width/2.0 - assert ortho_camera.position.y == height/2.0 + assert ortho_camera.position.x == width / 2.0 + assert ortho_camera.position.y == height / 2.0 @pytest.mark.parametrize("width, height", RENDER_TARGET_SIZES) @@ -144,8 +139,8 @@ def test_camera2d_from_camera_data_uses_render_target_size(window: Window, width assert ortho_camera.viewport_bottom == 0 assert ortho_camera.viewport_top == height - assert ortho_camera.position.x == width/2.0 - assert ortho_camera.position.y == height/2.0 + assert ortho_camera.position.x == width / 2.0 + assert ortho_camera.position.y == height / 2.0 def test_move_camera_and_project(window: Window):