From c3b75376fefe36f8246dd521bbae86584292e1da Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Wed, 13 Mar 2024 02:46:55 -0400 Subject: [PATCH 01/28] incomplete scale fix --- arcade/pymunk_physics_engine.py | 2 +- arcade/sprite/animated.py | 4 +-- arcade/sprite/base.py | 48 ++++++++++++++++++++++++++------- arcade/tilemap/tilemap.py | 2 +- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/arcade/pymunk_physics_engine.py b/arcade/pymunk_physics_engine.py index 87d797b81..f9bc85cee 100644 --- a/arcade/pymunk_physics_engine.py +++ b/arcade/pymunk_physics_engine.py @@ -182,7 +182,7 @@ def velocity_callback(my_body: pymunk.Body, my_gravity: Tuple[float, float], my_ # Set the physics shape to the sprite's hitbox poly = sprite.hit_box.points - scaled_poly = [[x * sprite.scale for x in z] for z in poly] + scaled_poly = [[x * sprite.scale_x for x in z] for z in poly] shape = pymunk.Poly(body, scaled_poly, radius=radius) # type: ignore # Set collision type, used in collision callbacks diff --git a/arcade/sprite/animated.py b/arcade/sprite/animated.py index a7c7230a8..d75fa9e0e 100644 --- a/arcade/sprite/animated.py +++ b/arcade/sprite/animated.py @@ -336,5 +336,5 @@ def update_animation(self, delta_time: float = 1 / 60) -> None: if self._texture is None: print("Error, no texture set") else: - self.width = self._texture.width * self.scale - self.height = self._texture.height * self.scale + self.width = self._texture.width * self.scale_x + self.height = self._texture.height * self.scale_x diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 41d2c2f33..a10b1ec4d 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -185,7 +185,7 @@ def height(self, new_value: float): # sprite_list._update_size(self) @property - def scale(self) -> float: + def scale_x(self) -> float: """ Get or set the sprite's x scale value or set both x & y scale to the same value. @@ -194,12 +194,37 @@ def scale(self) -> float: """ return self._scale[0] - @scale.setter - def scale(self, new_value: float): - if new_value == self._scale[0] and new_value == self._scale[1]: + @scale_x.setter + def scale_x(self, new_value: float): + if new_value == self._scale[0]: + return + + self._scale = new_value, self._scale[1] + self._hit_box.scale = self._scale + if self._texture: + self._width = self._texture.width * self._scale[0] + self._height = self._texture.height * self._scale[1] + + self.update_spatial_hash() + for sprite_list in self.sprite_lists: + sprite_list._update_size(self) + + @property + def scale_y(self) -> float: + """ + Get or set the sprite's x scale value or set both x & y scale to the same value. + + .. note:: Negative values are supported. They will flip & + mirror the sprite. + """ + return self._scale[1] + + @scale_y.setter + def scale_y(self, new_value: float): + if new_value == self._scale[1]: return - self._scale = new_value, new_value + self._scale = self._scale[0], new_value self._hit_box.scale = self._scale if self._texture: self._width = self._texture.width * self._scale[0] @@ -210,12 +235,15 @@ def scale(self, new_value: float): sprite_list._update_size(self) @property - def scale_xy(self) -> Point: + def scale(self) -> Point: """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): + @scale.setter + def scale(self, new_value: Union[Point, float]): + if isinstance(new_value, float): + new_value: Point = (new_value, new_value) + if new_value[0] == self._scale[0] and new_value[1] == self._scale[1]: return @@ -515,7 +543,7 @@ def rescale_relative_to_point(self, point: Point, factor: float) -> None: return # set the scale and, if this sprite has a texture, the size data - self.scale_xy = self._scale[0] * factor, self._scale[1] * factor + self.scale = self._scale[0] * factor, self._scale[1] * factor if self._texture: self._width = self._texture.width * self._scale[0] self._height = self._texture.height * self._scale[1] @@ -570,7 +598,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 = self._scale[0] * factor_x, self._scale[1] * factor_y if self._texture: self._width = self._texture.width * self._scale[0] self._height = self._texture.height * self._scale[1] diff --git a/arcade/tilemap/tilemap.py b/arcade/tilemap/tilemap.py index ae72f0d82..0eaf81190 100644 --- a/arcade/tilemap/tilemap.py +++ b/arcade/tilemap/tilemap.py @@ -590,7 +590,7 @@ def _create_sprite_from_tile( cast(List[Point], points), position=my_sprite.position, angle=my_sprite.angle, - scale=my_sprite.scale_xy, + scale=my_sprite.scale, ) if tile.animation: From 1fd283083c6b1f4230e1522a72880c7190071f8b Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Thu, 14 Mar 2024 00:50:09 -0400 Subject: [PATCH 02/28] fix a bunch of tests --- arcade/sprite/base.py | 8 ++--- tests/unit/sprite/test_sprite.py | 53 ++++++++++++++------------------ 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index a10b1ec4d..19c870669 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -195,7 +195,7 @@ def scale_x(self) -> float: return self._scale[0] @scale_x.setter - def scale_x(self, new_value: float): + def scale_x(self, new_value: Union[float, int]): if new_value == self._scale[0]: return @@ -220,7 +220,7 @@ def scale_y(self) -> float: return self._scale[1] @scale_y.setter - def scale_y(self, new_value: float): + def scale_y(self, new_value: Union[float, int]): if new_value == self._scale[1]: return @@ -240,8 +240,8 @@ def scale(self) -> Point: return self._scale @scale.setter - def scale(self, new_value: Union[Point, float]): - if isinstance(new_value, float): + def scale(self, new_value: Union[Point, float, int]): + if isinstance(new_value, float) or isinstance(new_value, int): new_value: Point = (new_value, new_value) if new_value[0] == self._scale[0] and new_value[1] == self._scale[1]: diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index 9e33b27c4..c5f6cb6be 100644 --- a/tests/unit/sprite/test_sprite.py +++ b/tests/unit/sprite/test_sprite.py @@ -219,20 +219,20 @@ def test_sprite_scale(window): # ensure normal scaling works correctly gold_1 = arcade.Sprite(":resources:/images/items/gold_1.png") - assert gold_1.scale == 1.0 + assert gold_1.scale == (1.0, 1.0) assert gold_1.width, gold_1.height == (64, 64) gold_1.scale = 2.0 - assert gold_1.scale == 2.0 + assert gold_1.scale == (2.0, 2.0) assert gold_1.width, gold_1.height == (128, 128) gold_1.scale *= 0.25 - assert gold_1.scale == 0.5 + assert gold_1.scale == (0.5, 0.5) assert gold_1.width, gold_1.height == (32, 32) # edge case: negative scale values are supported gold_1.scale *= -1.0 - assert gold_1.scale == - 0.5 + assert gold_1.scale == (-0.5, -0.5) assert gold_1.width, gold_1.height == (-32, -32) # visual spot check @@ -511,7 +511,7 @@ def sprite_64x64_at_position(x, y): window_center_y - 50 ) sprite_1.rescale_relative_to_point((0, 0), 3.31) - assert sprite_1.scale == 3.31 + assert sprite_1.scale == (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 @@ -528,10 +528,9 @@ def sprite_64x64_at_position(x, y): window_center_x + 10, window_center_y + 10 ) - sprite_2.scale_xy = 2.0, 1.0 + sprite_2.scale = (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 == (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 @@ -548,9 +547,9 @@ def sprite_64x64_at_position(x, y): window_center_x - 10, window_center_y - 10 ) - sprite_3.scale_xy = 0.5, 1.5 + sprite_3.scale = (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 == (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 @@ -560,8 +559,7 @@ def sprite_64x64_at_position(x, y): # expected: sprite does not move, but scale and dimensions change 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 == (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 @@ -571,8 +569,7 @@ def sprite_64x64_at_position(x, y): # expected : sprite doesn't move, but scale data doubled & negative 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 == (-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 @@ -585,7 +582,7 @@ def sprite_64x64_at_position(x, y): window_center_y + 81 ) sprite_6.rescale_relative_to_point((50, 40), 1.0) - assert sprite_6.scale == 1.0 + assert sprite_6.scale == (1.0, 1.0) assert sprite_6.center_x == window_center_x - 81 assert sprite_6.center_y == window_center_y + 81 assert sprite_6.width == 64 @@ -598,7 +595,7 @@ def sprite_64x64_at_position(x, y): window_center_y + 81 ) sprite_7.rescale_relative_to_point((50, 40), 1.0) - assert sprite_7.scale == 1.0 + assert sprite_7.scale == (1.0, 1.0) assert sprite_7.center_x == window_center_x - 81 assert sprite_7.center_y == window_center_y + 81 assert sprite_7.width == 64 @@ -613,7 +610,7 @@ def sprite_64x64_at_position(x, y): window_center_y + 81 ) sprite_8.rescale_relative_to_point(window_center, -1.0) - assert sprite_8.scale == -1.0 + assert sprite_8.scale == (-1.0, -1.0) assert sprite_8.center_x == window_center_x + 81 assert sprite_8.center_y == window_center_y - 81 assert sprite_8.width == -64 @@ -636,8 +633,7 @@ def sprite_64x64_at_position(x, y): window_center_y - 50 ) 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 == (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 @@ -648,10 +644,9 @@ def sprite_64x64_at_position(x, y): window_center_x + 10, window_center_y + 10 ) - sprite_2.scale_xy = 2.0, 1.0 + sprite_2.scale = (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 == (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 @@ -662,10 +657,9 @@ def sprite_64x64_at_position(x, y): window_center_x - 10, window_center_y - 10 ) - sprite_3.scale_xy = 0.5, 1.5 + sprite_3.scale = (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 == (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 @@ -675,7 +669,7 @@ def sprite_64x64_at_position(x, y): # expected: sprite does not move, but scale and dimensions change sprite_4 = sprite_64x64_at_position(*window_center) sprite_4.rescale_xy_relative_to_point(sprite_4.position, (2.0, 2.0)) - assert sprite_4.scale == 2.0 + assert sprite_4.scale == (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 @@ -688,7 +682,7 @@ def sprite_64x64_at_position(x, y): window_center_y + 81 ) sprite_5.rescale_xy_relative_to_point((50, 40), (1.0, 1.0)) - assert sprite_5.scale == 1.0 + assert sprite_5.scale == (1.0, 1.0) assert sprite_5.center_x == window_center_x - 81 assert sprite_5.center_y == window_center_y + 81 assert sprite_5.width == 64 @@ -698,8 +692,7 @@ def sprite_64x64_at_position(x, y): # expected : sprite doesn't move, but scale, width, & height < 0 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 == (-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 @@ -712,7 +705,7 @@ def sprite_64x64_at_position(x, y): window_center_y + 81 ) sprite_7.rescale_xy_relative_to_point((50, 40), (1.0, 1.0)) - assert sprite_7.scale == 1.0 + assert sprite_7.scale == (1.0, 1.0) assert sprite_7.center_x == window_center_x - 81 assert sprite_7.center_y == window_center_y + 81 assert sprite_7.width == 64 From 4a9081c424a6b98d418a5883e2d40c9098e5ab0a Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Thu, 14 Mar 2024 00:58:34 -0400 Subject: [PATCH 03/28] fix more tests --- tests/unit/sprite/test_sprite.py | 73 ++++++++++++++++---------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index c5f6cb6be..3303d6d0c 100644 --- a/tests/unit/sprite/test_sprite.py +++ b/tests/unit/sprite/test_sprite.py @@ -389,44 +389,46 @@ def test_sprite_scale_xy(window): # setting vector equivalent of previous scale doesn't change values sprite.scale = 1.0 - sprite.scale_xy = (1.0, 1.0) - assert sprite.scale == 1.0 - assert sprite.scale_xy == (1.0, 1.0) + sprite.scale = (1.0, 1.0) + assert sprite.scale == (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) + sprite.scale = 2.0, 2.0 + assert sprite.scale == (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 == 1.0 + sprite.scale = 1.0, 4.0 + assert sprite.scale == (1.0, 4.0) + assert sprite.scale_x == 1.0 + assert sprite.scale_y == 4.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 == 5.0 + sprite.scale = 5.0, 3.0 + assert sprite.scale == (5.0, 3.0) + assert sprite.scale_x == 5.0 + assert sprite.scale_y == 3.0 assert sprite.width, sprite.height == (100, 60) # edge case: setting scale_xy with x < 0 works correctly - sprite.scale_xy = (-1.0, 1.0) - assert sprite.scale == -1.0 + sprite.scale = -1.0, 1.0 + assert sprite.scale == (-1.0, -1.0) assert sprite.width == -20 assert sprite.height == 20 # edge case: setting scale_xy with y < 0 works correctly - sprite.scale_xy = (1.0, -1.0) - assert sprite.scale == 1.0 + sprite.scale = (1.0, -1.0) + assert sprite.scale_x == 1.0 + assert sprite.scale_y == -1.0 assert sprite.width == 20 assert sprite.height == -20 # edge case: setting scale_xy with x < 0, y < 0 works correctly - sprite.scale_xy = (-1.0, -1.0) - assert sprite.scale == -1.0 + sprite.scale = (-1.0, -1.0) + assert sprite.scale_x == -1.0 + assert sprite.scale_y == -1.0 assert sprite.width == -20 assert sprite.width == -20 @@ -435,55 +437,54 @@ def test_sprite_scale_resets_mismatched_xy_settings(window): sprite = arcade.SpriteSolidColor(20, 20, color=arcade.color.WHITE) # check if x dimension is properly reset - sprite.scale_xy = 3.0, 2.0 + sprite.scale = 3.0, 2.0 sprite.scale = 2.0 - assert sprite.scale == 2.0 - assert sprite.scale_xy == (2.0, 2.0) + assert sprite.scale == (2.0, 2.0) assert sprite.width == 40 assert sprite.height == 40 # check if y dimension is properly reset - sprite.scale_xy = 5.0, 3.0 + sprite.scale = 5.0, 3.0 sprite.scale = 5.0 - assert sprite.scale == 5.0 - assert sprite.scale_xy == (5.0, 5.0) + assert sprite.scale_x == 5.0 + assert sprite.scale == (5.0, 5.0) assert sprite.width == 100 assert sprite.height == 100 # check if both dimensions properly reset - sprite.scale_xy = 0.5, 4.0 + sprite.scale = 0.5, 4.0 sprite.scale = 1.0 - assert sprite.scale == 1.0 - assert sprite.scale_xy == (1.0, 1.0) + assert sprite.scale_x == 1.0 + assert sprite.scale == (1.0, 1.0) assert sprite.width == 20 assert sprite.height == 20 # edge case: setting negative values works - sprite.scale_xy = 0.5, 4.0 + sprite.scale = 0.5, 4.0 sprite.scale = -1.0 - assert sprite.scale == -1.0 - assert sprite.scale_xy == (-1.0, -1.0) + assert sprite.scale_x == -1.0 + assert sprite.scale == (-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 = -1.0, 1.0 sprite.scale = 2.0 - assert sprite.scale_xy == (2.0, 2.0) + assert sprite.scale == (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 = 1.0, -1.0 sprite.scale = 2.0 - assert sprite.scale_xy == (2.0, 2.0) + assert sprite.scale == (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 = -1.0, -1.0 sprite.scale = 2.0 - assert sprite.scale_xy == (2.0, 2.0) + assert sprite.scale == (2.0, 2.0) assert sprite.width == 40 assert sprite.height == 40 From 57082adbe5b6cb8deb026ad07bddeb17f57b8f07 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Thu, 14 Mar 2024 01:05:37 -0400 Subject: [PATCH 04/28] typo --- tests/unit/sprite/test_sprite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index 3303d6d0c..1190d195e 100644 --- a/tests/unit/sprite/test_sprite.py +++ b/tests/unit/sprite/test_sprite.py @@ -414,7 +414,7 @@ def test_sprite_scale_xy(window): # edge case: setting scale_xy with x < 0 works correctly sprite.scale = -1.0, 1.0 - assert sprite.scale == (-1.0, -1.0) + assert sprite.scale == (-1.0, 1.0) assert sprite.width == -20 assert sprite.height == 20 From 6ef286e3e8478daed3980ba9e19ed381be4655c7 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Thu, 14 Mar 2024 17:22:15 -0400 Subject: [PATCH 05/28] fix the one broken test and speed up .scale setter --- arcade/sprite/base.py | 18 +++++++++++------- tests/unit/sprite/test_sprite.py | 6 +++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 19c870669..34cbde6e3 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -241,17 +241,21 @@ def scale(self) -> Point: @scale.setter def scale(self, new_value: Union[Point, float, int]): - if isinstance(new_value, float) or isinstance(new_value, int): - new_value: Point = (new_value, new_value) + if isinstance(new_value, (float, int)): + new_value_scale: Point = (new_value, new_value) - if new_value[0] == self._scale[0] and new_value[1] == self._scale[1]: + else: # Treat it as some sort of iterable or sequence + x, y, *_ = new_value # type / length implicit check + new_value_scale = x, y + + if new_value_scale == self._scale: return - self._scale = new_value - self._hit_box.scale = self._scale + self._scale = new_value_scale + self._hit_box.scale = new_value_scale if self._texture: - self._width = self._texture.width * self._scale[0] - self._height = self._texture.height * self._scale[1] + self._width = self._texture.width * new_value_scale[0] + self._height = self._texture.height * new_value_scale[1] self.update_spatial_hash() diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index 1190d195e..c37fec92b 100644 --- a/tests/unit/sprite/test_sprite.py +++ b/tests/unit/sprite/test_sprite.py @@ -226,12 +226,12 @@ def test_sprite_scale(window): assert gold_1.scale == (2.0, 2.0) assert gold_1.width, gold_1.height == (128, 128) - gold_1.scale *= 0.25 + gold_1.scale = (x * 0.25 for x in gold_1.scale) assert gold_1.scale == (0.5, 0.5) assert gold_1.width, gold_1.height == (32, 32) # edge case: negative scale values are supported - gold_1.scale *= -1.0 + gold_1.scale = (x * -1 for x in gold_1.scale) assert gold_1.scale == (-0.5, -0.5) assert gold_1.width, gold_1.height == (-32, -32) @@ -250,7 +250,7 @@ def on_draw(): character_list.draw() def update(delta_time): - character_sprite.scale += 0.1 + character_sprite.scale = (x + 0.1 for x in gold_1.scale) window.on_draw = on_draw window.update = update From 97ec4206bf28b080e7f8899b8d247f45f5a289a9 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Thu, 14 Mar 2024 17:47:59 -0400 Subject: [PATCH 06/28] fix docstrings --- arcade/sprite/base.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 34cbde6e3..38232ccd2 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -187,7 +187,7 @@ def height(self, new_value: float): @property def scale_x(self) -> float: """ - Get or set the sprite's x scale value or set both x & y scale to the same value. + Get or set the sprite's x scale value. .. note:: Negative values are supported. They will flip & mirror the sprite. @@ -212,7 +212,7 @@ def scale_x(self, new_value: Union[float, int]): @property def scale_y(self) -> float: """ - Get or set the sprite's x scale value or set both x & y scale to the same value. + Get or set the sprite's y scale value. .. note:: Negative values are supported. They will flip & mirror the sprite. @@ -236,7 +236,10 @@ def scale_y(self, new_value: Union[float, int]): @property def scale(self) -> Point: - """Get or set the x & y scale of the sprite as a pair of values.""" + """Get or set the x & y scale of the sprite as a pair of values. + + .. note:: Negative values are supported. They will flip & + mirror the sprite.""" return self._scale @scale.setter From d62528d329771dacf72d764e66566fb9092acd8a Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Fri, 15 Mar 2024 01:37:23 -0400 Subject: [PATCH 07/28] Fix examples --- arcade/examples/particle_fireworks.py | 2 +- arcade/examples/sprite_health.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/arcade/examples/particle_fireworks.py b/arcade/examples/particle_fireworks.py index 361e83377..35f885aa4 100644 --- a/arcade/examples/particle_fireworks.py +++ b/arcade/examples/particle_fireworks.py @@ -359,7 +359,7 @@ def firework_spark_mutator(particle: FadeParticle): def rocket_smoke_mutator(particle: LifetimeParticle): - particle.scale = lerp(0.5, 3.0, particle.lifetime_elapsed / particle.lifetime_original) + particle.scale = lerp(0.5, 3.0, particle.lifetime_elapsed / particle.lifetime_original) # type: ignore def main(): diff --git a/arcade/examples/sprite_health.py b/arcade/examples/sprite_health.py index c1c8771da..7582ed069 100644 --- a/arcade/examples/sprite_health.py +++ b/arcade/examples/sprite_health.py @@ -52,7 +52,7 @@ def __init__(self, bar_list: arcade.SpriteList) -> None: scale=SPRITE_SCALING_PLAYER, ) self.indicator_bar: IndicatorBar = IndicatorBar( - self, bar_list, (self.center_x, self.center_y), scale=1.5, + self, bar_list, (self.center_x, self.center_y), scale=(1.5, 1.5), ) self.health: int = PLAYER_HEALTH @@ -98,7 +98,7 @@ def __init__( width: int = 100, height: int = 4, border_size: int = 4, - scale: float = 1.0, + scale: Tuple[float, float] = (1.0, 1.0), ) -> None: # Store the reference to the owner and the sprite list self.owner: Player = owner @@ -110,7 +110,7 @@ def __init__( self._center_x: float = 0.0 self._center_y: float = 0.0 self._fullness: float = 0.0 - self._scale: float = 1.0 + self._scale: Tuple[float, float] = (1.0, 1.0) # Create the boxes needed to represent the indicator bar self._background_box: arcade.SpriteSolidColor = arcade.SpriteSolidColor( @@ -206,8 +206,8 @@ def fullness(self, new_fullness: float) -> None: else: # Set the full_box to be visible incase it wasn't then update the bar self.full_box.visible = True - self.full_box.width = self._bar_width * new_fullness * self.scale - self.full_box.left = self._center_x - (self._bar_width / 2) * self.scale + self.full_box.width = self._bar_width * new_fullness * self.scale[0] + self.full_box.left = self._center_x - (self._bar_width / 2) * self.scale[0] @property def position(self) -> Tuple[float, float]: @@ -224,15 +224,15 @@ def position(self, new_position: Tuple[float, float]) -> None: self.full_box.position = new_position # Make sure full_box is to the left of the bar instead of the middle - self.full_box.left = self._center_x - (self._bar_width / 2) * self.scale + self.full_box.left = self._center_x - (self._bar_width / 2) * self.scale[0] @property - def scale(self) -> float: + def scale(self) -> Tuple[float, float]: """Returns the scale of the bar.""" return self._scale @scale.setter - def scale(self, value: float) -> None: + def scale(self, value: Tuple[float, float]) -> None: """Sets the new scale of the bar.""" # Check if the scale has changed. If so, change the bar's scale if value != self.scale: From 928e8fddcab39fe18a101a83157c3ff4d0323435 Mon Sep 17 00:00:00 2001 From: DigiDuncan Date: Tue, 16 Apr 2024 02:29:51 -0400 Subject: [PATCH 08/28] typing go brrr --- arcade/sprite/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 38232ccd2..61189916f 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -3,7 +3,7 @@ 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 AsFloat, Point, Color, RGBA255, RGBOrA255, PointList from arcade.color import BLACK, WHITE from arcade.hitbox import HitBox from arcade.texture import Texture @@ -195,7 +195,7 @@ def scale_x(self) -> float: return self._scale[0] @scale_x.setter - def scale_x(self, new_value: Union[float, int]): + def scale_x(self, new_value: AsFloat): if new_value == self._scale[0]: return @@ -220,7 +220,7 @@ def scale_y(self) -> float: return self._scale[1] @scale_y.setter - def scale_y(self, new_value: Union[float, int]): + def scale_y(self, new_value: AsFloat): if new_value == self._scale[1]: return @@ -243,7 +243,7 @@ def scale(self) -> Point: return self._scale @scale.setter - def scale(self, new_value: Union[Point, float, int]): + def scale(self, new_value: Point | AsFloat): if isinstance(new_value, (float, int)): new_value_scale: Point = (new_value, new_value) From 0309c91e3239ccc4849c31e2868e1a8977a347ea Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 19:54:42 -0400 Subject: [PATCH 09/28] Remove overly strict Point annotation --- arcade/sprite/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 553c15033..1c2bd7045 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -187,6 +187,7 @@ def size(self, new_value: Point): self._height = new_value[1] self.update_spatial_hash() + for sprite_list in self.sprite_lists: sprite_list._update_size(self) @@ -251,7 +252,7 @@ def scale(self) -> Point: @scale.setter def scale(self, new_value: Point | AsFloat): if isinstance(new_value, (float, int)): - new_value_scale: Point = (new_value, new_value) + new_value_scale = new_value, new_value else: # Treat it as some sort of iterable or sequence x, y, *_ = new_value # type / length implicit check From 012fd1ad26396f648e2a8cb5dc6377b2b13f9b77 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 20:30:44 -0400 Subject: [PATCH 10/28] Fix misuse of Point annotation which should be Point2 * Annotate HitBox scale __init__ arguments as Point2 * Annotate BasicSprite scale return as Point2 --- arcade/hitbox/base.py | 4 ++-- arcade/sprite/base.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/arcade/hitbox/base.py b/arcade/hitbox/base.py index afd098bbb..7ae0163a1 100644 --- a/arcade/hitbox/base.py +++ b/arcade/hitbox/base.py @@ -103,7 +103,7 @@ def __init__( self, points: Point2List, position: Point2 = (0.0, 0.0), - scale: tuple[float, float] = (1.0, 1.0), + scale: Point2 = (1.0, 1.0), ): self._points = points self._position = position @@ -249,7 +249,7 @@ def __init__( *, position: tuple[float, float] = (0.0, 0.0), angle: float = 0.0, - scale: tuple[float, float] = (1.0, 1.0), + scale: Point2 = (1.0, 1.0), ): super().__init__(points, position=position, scale=scale) self._angle: float = angle diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 1c2bd7045..c03b7f79d 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -242,7 +242,7 @@ def scale_y(self, new_value: AsFloat): sprite_list._update_size(self) @property - def scale(self) -> Point: + def scale(self) -> Point2: """Get or set the x & y scale of the sprite as a pair of values. .. note:: Negative values are supported. They will flip & From 7f0b2a7ed8dd9a6af3e90a6dd9ff238d8b85e461 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 20:36:02 -0400 Subject: [PATCH 11/28] Fix set_size tests w/ notes on problems with == in pyglet==2.1dev2 --- tests/unit/sprite/test_sprite.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index 67943c7e7..63ead38fa 100644 --- a/tests/unit/sprite/test_sprite.py +++ b/tests/unit/sprite/test_sprite.py @@ -51,14 +51,18 @@ def test_set_size(): assert sprite.size == (96, 128) assert sprite.width == 96 assert sprite.height == 128 - assert sprite.scale == 1.0 + assert sprite.scale == Vec2(1.0, 1.0) + # Not working as of pyglet 2.1dev2 + # assert sprite.scale == (1.0, 1.0) # Reduce to half width and height sprite.size = 48, 64 assert sprite.size == (48, 64) assert sprite.width == 48 assert sprite.height == 64 - assert sprite.scale == 0.5 + assert sprite.scale == Vec2(0.5, 0.5) + # Not working as of pyglet 2.1dev2 + # assert sprite.scale == (0.5, 0.5) @pytest.mark.parametrize('not_a_texture', [ From 5a3f42f9799db8b9267ce8405b5ef30ae67e0893 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 20:56:25 -0400 Subject: [PATCH 12/28] Add temp fix for pyglet 2.1dev2 --- arcade/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/arcade/__init__.py b/arcade/__init__.py index 6793ca180..b4904f845 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -58,6 +58,16 @@ def configure_logging(level: Optional[int] = None): # noinspection PyPep8 import pyglet +# TODO: Remove ASAP after pyglet >= 2.1dev2 is out +if pyglet.version == "2.1.dev2": + # Temporary monkeypatch via deletion since dev2 still includes + # overly-specific __eq__ behavior. Later pyglet commits restore + # equality with same-valued tuples by deleting the __eq__ methods. + from pyglet import math as _pyglet_math + del _pyglet_math.Vec2.__eq__ + del _pyglet_math.Vec3.__eq__ + del _pyglet_math.Vec4.__eq__ + # Env variable shortcut for headless mode if os.environ.get("ARCADE_HEADLESS"): pyglet.options["headless"] = True From 41ca51855701289e971dbe6a8ead0a42bf173449 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 21:04:48 -0400 Subject: [PATCH 13/28] Remove Vec2 from test_set_size() --- tests/unit/sprite/test_sprite.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index 63ead38fa..20efa3216 100644 --- a/tests/unit/sprite/test_sprite.py +++ b/tests/unit/sprite/test_sprite.py @@ -51,18 +51,14 @@ def test_set_size(): assert sprite.size == (96, 128) assert sprite.width == 96 assert sprite.height == 128 - assert sprite.scale == Vec2(1.0, 1.0) - # Not working as of pyglet 2.1dev2 - # assert sprite.scale == (1.0, 1.0) + assert sprite.scale == 1.0, 1.0 # Reduce to half width and height sprite.size = 48, 64 assert sprite.size == (48, 64) assert sprite.width == 48 assert sprite.height == 64 - assert sprite.scale == Vec2(0.5, 0.5) - # Not working as of pyglet 2.1dev2 - # assert sprite.scale == (0.5, 0.5) + assert sprite.scale == 0.5, 0.5 @pytest.mark.parametrize('not_a_texture', [ From 43d21742c52646167ac5b546ebf9954eb1557852 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 21:20:28 -0400 Subject: [PATCH 14/28] Update BasicSprite.scale and tests for it * Make .scale convert to Vec2 on return * Update scale unit tests to use tuples --- arcade/sprite/base.py | 4 ++-- tests/unit/sprite/test_sprite.py | 4 ++-- tests/unit/sprite/test_sprite_render.py | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index c03b7f79d..b7b5f4974 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -242,12 +242,12 @@ def scale_y(self, new_value: AsFloat): sprite_list._update_size(self) @property - def scale(self) -> Point2: + def scale(self) -> Vec2: """Get or set the x & y scale of the sprite as a pair of values. .. note:: Negative values are supported. They will flip & mirror the sprite.""" - return self._scale + return Vec2(*self._scale) @scale.setter def scale(self, new_value: Point | AsFloat): diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index 20efa3216..7ea14339b 100644 --- a/tests/unit/sprite/test_sprite.py +++ b/tests/unit/sprite/test_sprite.py @@ -51,14 +51,14 @@ def test_set_size(): assert sprite.size == (96, 128) assert sprite.width == 96 assert sprite.height == 128 - assert sprite.scale == 1.0, 1.0 + assert sprite.scale == (1.0, 1.0) # Reduce to half width and height sprite.size = 48, 64 assert sprite.size == (48, 64) assert sprite.width == 48 assert sprite.height == 64 - assert sprite.scale == 0.5, 0.5 + assert sprite.scale == (0.5, 0.5) @pytest.mark.parametrize('not_a_texture', [ diff --git a/tests/unit/sprite/test_sprite_render.py b/tests/unit/sprite/test_sprite_render.py index 443248683..5594bf33d 100644 --- a/tests/unit/sprite/test_sprite_render.py +++ b/tests/unit/sprite/test_sprite_render.py @@ -202,20 +202,20 @@ def test_render_scaled(window): # ensure normal scaling works correctly gold_1 = arcade.Sprite(":resources:/images/items/gold_1.png") - assert gold_1.scale == 1.0 + assert gold_1.scale == (1.0, 1.0) assert gold_1.width, gold_1.height == (64, 64) gold_1.scale = 2.0 - assert gold_1.scale == 2.0 + assert gold_1.scale == (2.0, 2.0) assert gold_1.width, gold_1.height == (128, 128) gold_1.scale *= 0.25 - assert gold_1.scale == 0.5 + assert gold_1.scale == (0.5, 0.5) assert gold_1.width, gold_1.height == (32, 32) # edge case: negative scale values are supported gold_1.scale *= -1.0 - assert gold_1.scale == - 0.5 + assert gold_1.scale == (-0.5, -0.5) assert gold_1.width, gold_1.height == (-32, -32) # visual spot check From 54678c62ab84ebd90f204364fa383166c07a2cc1 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 21:23:13 -0400 Subject: [PATCH 15/28] Fix formatting to make CI happy --- arcade/__init__.py | 1 + arcade/sprite/base.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/arcade/__init__.py b/arcade/__init__.py index b4904f845..2d76250c4 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -64,6 +64,7 @@ def configure_logging(level: Optional[int] = None): # overly-specific __eq__ behavior. Later pyglet commits restore # equality with same-valued tuples by deleting the __eq__ methods. from pyglet import math as _pyglet_math + del _pyglet_math.Vec2.__eq__ del _pyglet_math.Vec3.__eq__ del _pyglet_math.Vec4.__eq__ diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index b7b5f4974..fe8cffe57 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -182,7 +182,9 @@ def size(self) -> Point: @size.setter def size(self, new_value: Point): if new_value[0] != self._width or new_value[1] != self._height: - self._scale = Vec2(new_value[0] / self._texture.width, new_value[1] / self._texture.height) + self._scale = Vec2( + new_value[0] / self._texture.width, new_value[1] / self._texture.height + ) self._width = new_value[0] self._height = new_value[1] From fa542b2c2c672e10213ba8189701522e58123e3c Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 21:33:03 -0400 Subject: [PATCH 16/28] Add optimized validation for BasicSprite.size + tests --- arcade/sprite/base.py | 28 ++++++++++++++++++---------- tests/unit/sprite/test_sprite.py | 8 ++++++++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index fe8cffe57..1fd4b87fa 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -60,9 +60,10 @@ def __init__( self._position = (center_x, center_y) self._depth = 0.0 self._texture = texture - self._width = texture.width * scale - self._height = texture.height * scale - self._scale = Vec2(scale, scale) + width, height = texture.size + self._width = width * scale + self._height = height * scale + self._scale = (scale, scale) self._visible = bool(visible) self._color: Color = WHITE self.sprite_lists: list["SpriteList"] = [] @@ -180,13 +181,20 @@ def size(self) -> Point: return self._width, self._height @size.setter - def size(self, new_value: Point): - if new_value[0] != self._width or new_value[1] != self._height: - self._scale = Vec2( - new_value[0] / self._texture.width, new_value[1] / self._texture.height - ) - self._width = new_value[0] - self._height = new_value[1] + def size(self, new_value: Point2): + try: + width, height, *bad = new_value + assert not bad + except (AssertionError, ValueError): + raise ValueError("size must be a tuple-like object which unpacks to exactly 2 coordinates") + except TypeError: + raise TypeError("size must be a tuple-like object which unpacks to exactly 2 coordinates") + + if width != self._width or height != self._height: + texture_width, texture_height = self._texture.size + self._scale = width / texture_width, height / texture_height + self._width = width + self._height = height self.update_spatial_hash() diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index 7ea14339b..f6f6c695f 100644 --- a/tests/unit/sprite/test_sprite.py +++ b/tests/unit/sprite/test_sprite.py @@ -60,6 +60,14 @@ def test_set_size(): assert sprite.height == 64 assert sprite.scale == (0.5, 0.5) + # Validate size data + with pytest.raises(ValueError): + sprite.size = (1,) + with pytest.raises(ValueError): + sprite.size = (0, 1, 2) + with pytest.raises(TypeError): + sprite.size = 1 + @pytest.mark.parametrize('not_a_texture', [ 1, "not_a_texture", (1, 2, 3) From 30e92e1f99367648e5072d5ca7b75be9b3f5c1f9 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 22:02:33 -0400 Subject: [PATCH 17/28] Optimize & clean up BasicSprite.scale_x setter * Remove if check around texture since we are guaranteed to have one now * Rename scale_x argument from new_value to new_scale_x * Unpack self._scale into old_scale_* * Remove redundant scale setting for y * Apply new scale to the hitbox first to raise exceptions earlier --- arcade/sprite/base.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 1fd4b87fa..6a4317d51 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -212,15 +212,17 @@ def scale_x(self) -> float: return self._scale[0] @scale_x.setter - def scale_x(self, new_value: AsFloat): - if new_value == self._scale[0]: + def scale_x(self, new_scale_x: AsFloat): + old_scale_x, old_scale_y = self._scale + if new_scale_x == old_scale_x: return - self._scale = Vec2(new_value, self._scale[1]) - self._hit_box.scale = self._scale - if self._texture: - self._width = self._texture.width * self._scale[0] - self._height = self._texture.height * self._scale[1] + new_scale = (new_scale_x, old_scale_y) + + # Apply scale to hitbox first to raise any exceptions quickly + self._hit_box.scale = new_scale + self._scale = new_scale + self._width = self._texture.width * new_scale_x self.update_spatial_hash() for sprite_list in self.sprite_lists: From 06d9ed804195ef2b68a6b35d0513f1fcbf1a3d38 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 22:08:36 -0400 Subject: [PATCH 18/28] Optimize & clean up BasicSprite.scale_y setter * Remove if check around texture since we are guaranteed to have one now * Rename scale_x argument from new_value to new_scale_x * Unpack self._scale into old_scale_* * Remove redundant scale setting for x * Apply new scale to the hitbox first to raise exceptions earlier --- arcade/sprite/base.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 6a4317d51..3944338c5 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -239,15 +239,17 @@ def scale_y(self) -> float: return self._scale[1] @scale_y.setter - def scale_y(self, new_value: AsFloat): - if new_value == self._scale[1]: + def scale_y(self, new_scale_y: AsFloat): + old_scale_x, old_scale_y = self._scale + if new_scale_y == old_scale_y: return - self._scale = Vec2(self._scale[0], new_value) - self._hit_box.scale = self._scale - if self._texture: - self._width = self._texture.width * self._scale[0] - self._height = self._texture.height * self._scale[1] + new_scale = (old_scale_x, new_scale_y) + + # Apply scale to hitbox first to raise any exceptions quickly + self._hit_box.scale = new_scale + self._scale = new_scale + self._height = self._texture.height * new_scale_y self.update_spatial_hash() for sprite_list in self.sprite_lists: From ccd63211c02426d56cd20fe8af609523da2080cb Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 22:22:02 -0400 Subject: [PATCH 19/28] Optimize and clean up scale setter * Rename new_value to new_scale * Assign to scale_x and scale_y instead of immediate tuple creation * Add comments about hot code path asking not to DRY it * Add exception checks for unpack * Reorder and reduce use of dot and index acccess * Remove extra line --- arcade/sprite/base.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 3944338c5..f4fc7477c 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -264,25 +264,31 @@ def scale(self) -> Vec2: return Vec2(*self._scale) @scale.setter - def scale(self, new_value: Point | AsFloat): - if isinstance(new_value, (float, int)): - new_value_scale = new_value, new_value + def scale(self, new_scale: Point2 | AsFloat): + if isinstance(new_scale, (float, int)): + scale_x = new_scale + scale_y = new_scale else: # Treat it as some sort of iterable or sequence - x, y, *_ = new_value # type / length implicit check - new_value_scale = x, y - - if new_value_scale == self._scale: + # Don't abstract this. Keep it here since it's a hot code path + try: + scale_x, scale_y = new_scale # type / length implicit check + except ValueError: + raise ValueError("scale must be a tuple-like object which unpacks to exactly 2 coordinates") + except TypeError: + raise TypeError("scale must be a tuple-like object which unpacks to exactly 2 coordinates") + + new_scale = scale_x, scale_y + if new_scale == self._scale: return - self._scale = new_value_scale - self._hit_box.scale = new_value_scale - if self._texture: - self._width = self._texture.width * new_value_scale[0] - self._height = self._texture.height * new_value_scale[1] + self._hit_box.scale = new_scale + tex_width, tex_height = self._texture.size + self._scale = new_scale + self._width = tex_width * scale_x + self._height = tex_height * scale_y self.update_spatial_hash() - for sprite_list in self.sprite_lists: sprite_list._update_size(self) From de3d71238b94932b79c834724791294f06134997 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 22:25:38 -0400 Subject: [PATCH 20/28] Revert use of assert in size.setter since digi was right --- arcade/sprite/base.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index f4fc7477c..3b41286a3 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -183,9 +183,8 @@ def size(self) -> Point: @size.setter def size(self, new_value: Point2): try: - width, height, *bad = new_value - assert not bad - except (AssertionError, ValueError): + width, height = new_value + except ValueError: raise ValueError("size must be a tuple-like object which unpacks to exactly 2 coordinates") except TypeError: raise TypeError("size must be a tuple-like object which unpacks to exactly 2 coordinates") From b98d46a90a025fdabc50c48d3ea97eeae7bee616 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 22:49:58 -0400 Subject: [PATCH 21/28] Clean up rescale_relative_to_point's insides * Significantly redeuce dot and index access in rescale_relative_to_point * Precache re-used quantities * Comments for clarity --- arcade/sprite/base.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 3b41286a3..45443960b 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -583,19 +583,26 @@ def rescale_relative_to_point(self, point: Point, factor: float) -> None: return # set the scale and, if this sprite has a texture, the size data - self.scale = self._scale[0] * factor, self._scale[1] * factor - if self._texture: - self._width = self._texture.width * self._scale[0] - self._height = self._texture.height * self._scale[1] + old_scale_x, old_scale_y = self._scale + new_scale_x = old_scale_x * factor + new_scale_y = old_scale_y * factor + self.scale = new_scale_x, new_scale_y - # detect the edge case where distance to multiply is zero - position_changed = point != self._position + tex_width, tex_height = self._texture.size + self._width = tex_width * new_scale_x + self._height = tex_height * new_scale_y + + # If the scaling point is the sprite's center, it doesn't move + old_position = self._position + position_changed = point != old_position # Stored to use below # be lazy about math; only do it if we have to if position_changed: + point_x, point_y = point + old_x, old_y = old_position self.position = ( - (self._position[0] - point[0]) * factor + point[0], - (self._position[1] - point[1]) * factor + point[1], + (old_x - point_x) * factor + point_x, + (old_y - point_y) * factor + point_y, ) # rebuild all spatial metadata From 176b5b96d963273a893ab9cf9e7a031365d837a9 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 23:13:08 -0400 Subject: [PATCH 22/28] Update rescale_relative_to_point's docstring * Use scale instead of removed scale_xy * Convert to more pyglet/Google-style --- arcade/sprite/base.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 45443960b..eb61e6cb2 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -561,12 +561,11 @@ def update_animation(self, delta_time: float = 1 / 60) -> None: # --- Scale methods ----- def rescale_relative_to_point(self, point: Point, factor: float) -> None: - """ - Rescale the sprite and its distance from the passed point. + """Rescale the sprite and its distance from the passed point. This function does two things: - 1. Multiply both values in the sprite's :py:attr:`~scale_xy` + 1. Multiply both values in the sprite's :py:attr:`~scale` value by ``factor``. 2. Scale the distance between the sprite and ``point`` by ``factor``. @@ -574,9 +573,14 @@ def rescale_relative_to_point(self, point: Point, factor: float) -> None: If ``point`` equals the sprite's :py:attr:`~position`, the distance will be zero and the sprite will not move. - :param point: The reference point for rescaling. - :param factor: Multiplier for sprite scale & distance to point. - :return: + Args: + point: + The point to scale relative to. + factor: + A multiplier for both the sprite scale and its distance + from the point. Note that although factor may be negative, + it may have unexpected effects. See :py:` + """ # abort if the multiplier wouldn't do anything if factor == 1.0: From 9254388ed2d0fd37940dc657b78717382a0df5f4 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 23:45:28 -0400 Subject: [PATCH 23/28] Unify rescale*_relative_to_point methods * Add vector unpack check logic to rescale_relative_to_point * Update signature annotations * Update docstring * Rename factor argument to scale_by * Delete body of rescale_xy_relative_to_point * Update docstring to point to rescale_relative_to_point with deprecation * Add @warning wrapper to rescale_xy_relative_to_point --- arcade/sprite/base.py | 89 +++++++++++++++++--------------- tests/unit/sprite/test_sprite.py | 16 +++--- 2 files changed, 55 insertions(+), 50 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index eb61e6cb2..cb0c76d3c 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -7,7 +7,7 @@ from arcade.color import BLACK, WHITE from arcade.hitbox import HitBox from arcade.texture import Texture -from arcade.utils import copy_dunders_unimplemented +from arcade.utils import copy_dunders_unimplemented, ReplacementWarning, warning from pyglet.math import Vec2 @@ -560,37 +560,59 @@ def update_animation(self, delta_time: float = 1 / 60) -> None: # --- Scale methods ----- - def rescale_relative_to_point(self, point: Point, factor: float) -> None: + @warn + def rescale_relative_to_point(self, point: Point, scale_by: AsFloat | Point2) -> None: """Rescale the sprite and its distance from the passed point. This function does two things: 1. Multiply both values in the sprite's :py:attr:`~scale` - value by ``factor``. + value by the values in ``scale_by``: + + * If ``scale_by`` is an :py:class:`int` or :py:class:`float`, + use it for both the x and y axes + * If ``scale_by`` is a tuple-like object which unpacks to + two numbers, then use + * Otherwise, raise an exception + 2. Scale the distance between the sprite and ``point`` by ``factor``. - If ``point`` equals the sprite's :py:attr:`~position`, - the distance will be zero and the sprite will not move. + .. note:: If ``point`` equals the sprite's :py:attr:`~position`, + the distance will be zero and the sprite won't move. Args: point: The point to scale relative to. - factor: + scale_by: A multiplier for both the sprite scale and its distance from the point. Note that although factor may be negative, it may have unexpected effects. See :py:` """ # abort if the multiplier wouldn't do anything - if factor == 1.0: - return + if isinstance(scale_by, (float, int)): + if scale_by == 1.0: + return + factor_x = scale_by + factor_y = scale_by + else: + try: + factor_x, factor_y = scale_by + if factor_x == 1.0 and factor_y == 1.0: + return + except ValueError: + raise ValueError( + 'factor must be a float, int, or tuple-like which unpacks as two float-like values') + except TypeError: + raise TypeError( + 'factor must be a float, int, or tuple-like unpacks as two float-like values') # set the scale and, if this sprite has a texture, the size data old_scale_x, old_scale_y = self._scale - new_scale_x = old_scale_x * factor - new_scale_y = old_scale_y * factor - self.scale = new_scale_x, new_scale_y + new_scale_x = old_scale_x * factor_x + new_scale_y = old_scale_y * factor_y + self._scale = new_scale_x, new_scale_y tex_width, tex_height = self._texture.size self._width = tex_width * new_scale_x @@ -605,8 +627,8 @@ def rescale_relative_to_point(self, point: Point, factor: float) -> None: point_x, point_y = point old_x, old_y = old_position self.position = ( - (old_x - point_x) * factor + point_x, - (old_y - point_y) * factor + point_y, + (old_x - point_x) * factor_x + point_x, + (old_y - point_y) * factor_y + point_y, ) # rebuild all spatial metadata @@ -616,9 +638,15 @@ def rescale_relative_to_point(self, point: Point, factor: float) -> None: if position_changed: sprite_list._update_position(self) + @warning(warning_type=ReplacementWarning, new_name="rescale_relative_to_point") def rescale_xy_relative_to_point(self, point: Point, factors_xy: Iterable[float]) -> None: - """ - Rescale the sprite and its distance from the passed point. + """Rescale the sprite and its distance from the passed point. + + .. deprecated:: 3.0 + Use :py:meth:`.rescale_relative_to_point` instead. + + This was added during the 3.0 development cycle before scale was + made into a vector quantitity. This method can scale by different amounts on each axis. To scale along only one axis, set the other axis to ``1.0`` in @@ -641,33 +669,10 @@ def rescale_xy_relative_to_point(self, point: Point, factors_xy: Iterable[float] ``point``. :return: """ - # exit early if nothing would change - factor_x, factor_y = factors_xy - if factor_x == 1.0 and factor_y == 1.0: - return - - # set the scale and, if this sprite has a texture, the size data - self.scale = 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] - - # detect the edge case where the distance to multiply is 0 - position_changed = point != self._position - - # be lazy about math; only do it if we have to - if position_changed: - self.position = ( - (self._position[0] - point[0]) * factor_x + point[0], - (self._position[1] - point[1]) * factor_y + point[1], - ) - - # rebuild all spatial metadata - self.update_spatial_hash() - for sprite_list in self.sprite_lists: - sprite_list._update_size(self) - if position_changed: - sprite_list._update_position(self) + self.rescale_relative_to_point( + point, + factors_xy # type: ignore + ) # ---- Utility Methods ---- diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index f6f6c695f..ae470699d 100644 --- a/tests/unit/sprite/test_sprite.py +++ b/tests/unit/sprite/test_sprite.py @@ -386,7 +386,7 @@ def sprite_64x64_at_position(x, y): assert sprite_8.height == -64 -def test_rescale_xy_relative_to_point(window): +def test_rescale_relative_to_point_with_vec_quants(window): window_center = window.width // 2, window.height // 2 window_center_x, window_center_y = window_center @@ -401,7 +401,7 @@ def sprite_64x64_at_position(x, y): window_center_x + 50, window_center_y - 50 ) - sprite_1.rescale_xy_relative_to_point((0, 0), (3.31, 3.31)) + sprite_1.rescale_relative_to_point((0, 0), (3.31, 3.31)) assert sprite_1.scale == (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 @@ -414,7 +414,7 @@ def sprite_64x64_at_position(x, y): window_center_y + 10 ) sprite_2.scale = (2.0, 1.0) - sprite_2.rescale_xy_relative_to_point(window_center, (2.0, 2.0)) + sprite_2.rescale_relative_to_point(window_center, (2.0, 2.0)) assert sprite_2.scale == (4.0, 2.0) assert sprite_2.center_x == window_center_x + 20 assert sprite_2.center_y == window_center_y + 20 @@ -427,7 +427,7 @@ def sprite_64x64_at_position(x, y): window_center_y - 10 ) sprite_3.scale = (0.5, 1.5) - sprite_3.rescale_xy_relative_to_point(window_center, (3.0, 3.0)) + sprite_3.rescale_relative_to_point(window_center, (3.0, 3.0)) assert sprite_3.scale == (1.5, 4.5) assert sprite_3.center_x == window_center_x - 30 assert sprite_3.center_y == window_center_y - 30 @@ -437,7 +437,7 @@ def sprite_64x64_at_position(x, y): # edge case: point == sprite center, factor > 1 # expected: sprite does not move, but scale and dimensions change sprite_4 = sprite_64x64_at_position(*window_center) - sprite_4.rescale_xy_relative_to_point(sprite_4.position, (2.0, 2.0)) + sprite_4.rescale_relative_to_point(sprite_4.position, (2.0, 2.0)) assert sprite_4.scale == (2.0, 2.0) assert sprite_4.center_x == window_center_x assert sprite_4.center_y == window_center_y @@ -450,7 +450,7 @@ def sprite_64x64_at_position(x, y): window_center_x - 81, window_center_y + 81 ) - sprite_5.rescale_xy_relative_to_point((50, 40), (1.0, 1.0)) + sprite_5.rescale_relative_to_point((50, 40), (1.0, 1.0)) assert sprite_5.scale == (1.0, 1.0) assert sprite_5.center_x == window_center_x - 81 assert sprite_5.center_y == window_center_y + 81 @@ -460,7 +460,7 @@ def sprite_64x64_at_position(x, y): # edge case: point == sprite center, negative factor # expected : sprite doesn't move, but scale, width, & height < 0 sprite_6 = sprite_64x64_at_position(*window_center) - sprite_6.rescale_xy_relative_to_point(sprite_6.position, (-2.0, -2.0)) + sprite_6.rescale_relative_to_point(sprite_6.position, (-2.0, -2.0)) assert sprite_6.scale == (-2.0, -2.0) assert sprite_6.center_x == window_center_x assert sprite_6.center_y == window_center_y @@ -473,7 +473,7 @@ def sprite_64x64_at_position(x, y): window_center_x - 81, window_center_y + 81 ) - sprite_7.rescale_xy_relative_to_point((50, 40), (1.0, 1.0)) + sprite_7.rescale_relative_to_point((50, 40), (1.0, 1.0)) assert sprite_7.scale == (1.0, 1.0) assert sprite_7.center_x == window_center_x - 81 assert sprite_7.center_y == window_center_y + 81 From 09b6b46e046f3e91599d7242a115a0cd0bd8e59d Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 23:46:26 -0400 Subject: [PATCH 24/28] Apply auto-formatting --- arcade/sprite/base.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index cb0c76d3c..a7b6ff113 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -185,9 +185,13 @@ def size(self, new_value: Point2): try: width, height = new_value except ValueError: - raise ValueError("size must be a tuple-like object which unpacks to exactly 2 coordinates") + raise ValueError( + "size must be a tuple-like object which unpacks to exactly 2 coordinates" + ) except TypeError: - raise TypeError("size must be a tuple-like object which unpacks to exactly 2 coordinates") + raise TypeError( + "size must be a tuple-like object which unpacks to exactly 2 coordinates" + ) if width != self._width or height != self._height: texture_width, texture_height = self._texture.size @@ -273,9 +277,13 @@ def scale(self, new_scale: Point2 | AsFloat): try: scale_x, scale_y = new_scale # type / length implicit check except ValueError: - raise ValueError("scale must be a tuple-like object which unpacks to exactly 2 coordinates") + raise ValueError( + "scale must be a tuple-like object which unpacks to exactly 2 coordinates" + ) except TypeError: - raise TypeError("scale must be a tuple-like object which unpacks to exactly 2 coordinates") + raise TypeError( + "scale must be a tuple-like object which unpacks to exactly 2 coordinates" + ) new_scale = scale_x, scale_y if new_scale == self._scale: @@ -603,10 +611,12 @@ def rescale_relative_to_point(self, point: Point, scale_by: AsFloat | Point2) -> return except ValueError: raise ValueError( - 'factor must be a float, int, or tuple-like which unpacks as two float-like values') + "factor must be a float, int, or tuple-like which unpacks as two float-like values" + ) except TypeError: raise TypeError( - 'factor must be a float, int, or tuple-like unpacks as two float-like values') + "factor must be a float, int, or tuple-like unpacks as two float-like values" + ) # set the scale and, if this sprite has a texture, the size data old_scale_x, old_scale_y = self._scale @@ -669,10 +679,7 @@ def rescale_xy_relative_to_point(self, point: Point, factors_xy: Iterable[float] ``point``. :return: """ - self.rescale_relative_to_point( - point, - factors_xy # type: ignore - ) + self.rescale_relative_to_point(point, factors_xy) # type: ignore # ---- Utility Methods ---- From bc8833295817bee2a1d55ff42cc0936d2bca633d Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 23:48:02 -0400 Subject: [PATCH 25/28] Fix typo --- arcade/sprite/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index a7b6ff113..f0ca3b4ac 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -568,7 +568,6 @@ def update_animation(self, delta_time: float = 1 / 60) -> None: # --- Scale methods ----- - @warn def rescale_relative_to_point(self, point: Point, scale_by: AsFloat | Point2) -> None: """Rescale the sprite and its distance from the passed point. From db73158515a6b9cf459b3dfb35f4b12a363e8270 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 23:50:44 -0400 Subject: [PATCH 26/28] Make Sphinx build --- arcade/sprite/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index f0ca3b4ac..1aed397ef 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -573,7 +573,7 @@ def rescale_relative_to_point(self, point: Point, scale_by: AsFloat | Point2) -> This function does two things: - 1. Multiply both values in the sprite's :py:attr:`~scale` + 1. Multiply both values in the sprite's :py:attr:`.scale` value by the values in ``scale_by``: * If ``scale_by`` is an :py:class:`int` or :py:class:`float`, @@ -585,7 +585,7 @@ def rescale_relative_to_point(self, point: Point, scale_by: AsFloat | Point2) -> 2. Scale the distance between the sprite and ``point`` by ``factor``. - .. note:: If ``point`` equals the sprite's :py:attr:`~position`, + .. note:: If ``point`` equals the sprite's :py:attr:`.position` the distance will be zero and the sprite won't move. Args: @@ -594,7 +594,7 @@ def rescale_relative_to_point(self, point: Point, scale_by: AsFloat | Point2) -> scale_by: A multiplier for both the sprite scale and its distance from the point. Note that although factor may be negative, - it may have unexpected effects. See :py:` + it may have unexpected effects. """ # abort if the multiplier wouldn't do anything From a2e6cd48efafa86fe8230b1e5e7d2511b92cc97b Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 30 Jun 2024 23:51:17 -0400 Subject: [PATCH 27/28] Fix use of Point with Point2 --- arcade/sprite/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 1aed397ef..57b850058 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -568,7 +568,7 @@ def update_animation(self, delta_time: float = 1 / 60) -> None: # --- Scale methods ----- - def rescale_relative_to_point(self, point: Point, scale_by: AsFloat | Point2) -> None: + def rescale_relative_to_point(self, point: Point2, scale_by: AsFloat | Point2) -> None: """Rescale the sprite and its distance from the passed point. This function does two things: From 1f35b0d325c152c039f53547b0ee3cb0453db78a Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Mon, 1 Jul 2024 00:19:26 -0400 Subject: [PATCH 28/28] Add detailed explanation of seemingly strange Vec2 usage --- arcade/sprite/base.py | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 57b850058..88e804696 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -262,8 +262,45 @@ def scale_y(self, new_scale_y: AsFloat): def scale(self) -> Vec2: """Get or set the x & y scale of the sprite as a pair of values. - .. note:: Negative values are supported. They will flip & - mirror the sprite.""" + You may set it to either a single value or a pair of values: + + .. list-table:: + :header-rows: 0 + + * - Single value + - ``sprite.scale = 2.0`` + + * - Tuple or :py:class:`~pyglet,math.Vec2` + - ``sprite.scale = (1.0, 3.0)`` + + The two-channel version is useful for making health bars and + other indicators. + + .. note:: Returns a :py:class:`pyglet.math.Vec2` for + compatibility. + + Arcade versions lower than 3,0 used one or both of the following + for scale: + + * A single :py:class:`float` on versions <= 2.6 + * A ``scale_xy`` property and exposing only the x component + on some intermediate dev releases + + Although scale is internally stored as a :py:class:`tuple`, we + return a :py:class:`pyglet.math.Vec2` to allow the in-place + operators to work in addition to setting values directly: + + * Old-style (``sprite.scale *= 2.0``) + * New-style (``sprite.scale *= 2.0, 2.0``) + + .. note:: Negative scale values are supported. + + This applies to both single-axis and dual-axis. + Negatives will flip & mirror the sprite, but the + with will use :py:func:`abs` to report total width + and height instead of negatives. + + """ return Vec2(*self._scale) @scale.setter