diff --git a/ppb/camera.py b/ppb/camera.py index 10528c6e..6a9d58ee 100644 --- a/ppb/camera.py +++ b/ppb/camera.py @@ -96,10 +96,18 @@ def in_frame(self, sprite: BaseSprite) -> bool: self.frame_bottom >= sprite.top ) - def translate_to_frame(self, point:Vector) -> Vector: + def translate_to_frame(self, point: Vector) -> Vector: + """ + Converts a vector from pixel-based window to in-game coordinate space + """ offset = (point - self.viewport_offset) * (1/self.pixel_ratio) - return self.position + offset + loc = self.position + offset + return loc.update(y=-loc.y) - def translate_to_viewport(self, point:Vector) -> Vector: + def translate_to_viewport(self, point: Vector) -> Vector: + """ + Converts a vector from in-game to pixel-based window coordinate space + """ + point = point.update(y=-point.y) offset = (point - self.position) * self.pixel_ratio return self.viewport_offset + offset diff --git a/ppb/sprites.py b/ppb/sprites.py index 259d3b3b..d9bc3f06 100644 --- a/ppb/sprites.py +++ b/ppb/sprites.py @@ -2,12 +2,14 @@ from numbers import Number from os.path import realpath from pathlib import Path -from typing import Dict, Iterable, AnyStr, Sequence +from typing import Dict, Iterable, Sequence from typing import Union from ppb import Vector from ppb.events import EventMixin +import ppb_vector.vector2 + TOP = "top" BOTTOM = "bottom" @@ -26,7 +28,7 @@ class Side: BOTTOM: ('y', 1) } - def __init__(self, parent: 'BaseSprite',side: AnyStr): + def __init__(self, parent: 'BaseSprite', side: str): self.side = side self.parent = parent @@ -66,11 +68,14 @@ def __gt__(self, other): def __lt__(self, other): return self.value < other + def _lookup_side(self, side): + dimension, sign = self.sides[side] + return dimension, sign * self.parent._offset_value + @property def value(self): - coordinate, multiplier = self.sides[self.side] - offset = self.parent._offset_value - return self.parent.position[coordinate] + (offset * multiplier) + dimension, offset = self._lookup_side(self.side) + return self.parent.position[dimension] + offset @property def top(self): @@ -80,8 +85,7 @@ def top(self): @top.setter def top(self, value): self._attribute_gate(TOP, [TOP, BOTTOM]) - setattr(self.parent, self.side, value[0]) - self.parent.top = value[1] + self.parent.position = self._mk_update_vector_side(TOP, value) @property def bottom(self): @@ -91,8 +95,7 @@ def bottom(self): @bottom.setter def bottom(self, value): self._attribute_gate(BOTTOM, [TOP, BOTTOM]) - setattr(self.parent, self.side, value[0]) - self.parent.bottom = value[1] + self.parent.position = self._mk_update_vector_side(BOTTOM, value) @property def left(self): @@ -102,8 +105,7 @@ def left(self): @left.setter def left(self, value): self._attribute_gate(LEFT, [LEFT, RIGHT]) - setattr(self.parent, self.side, value[1]) - self.parent.left = value[0] + self.parent.position = self._mk_update_vector_side(LEFT, value) @property def right(self): @@ -113,8 +115,7 @@ def right(self): @right.setter def right(self, value): self._attribute_gate(RIGHT, [LEFT, RIGHT]) - setattr(self.parent, self.side, value[1]) - self.parent.right = value[0] + self.parent.position = self._mk_update_vector_side(RIGHT, value) @property def center(self): @@ -125,12 +126,46 @@ def center(self): @center.setter def center(self, value): - if self.side in (TOP, BOTTOM): - setattr(self.parent, self.side, value[1]) - self.parent.center.x = value[0] - else: - setattr(self.parent, self.side, value[0]) - self.parent.position.y = value[1] + self.parent.position = self._mk_update_vector_center(value) + + def _mk_update_vector_side(self, attribute, value): + """ + Calculate the updated vector for the given corner + """ + value = Vector(value) + assert attribute != 'center' + # Does a bunch of dynamc resolution: + # Sprite.top.left + # ^ ^ attribute + # self.side + self_dimension, self_offset = self._lookup_side(self.side) + + attr_dimension, attr_offset = self._lookup_side(attribute) + + assert self_dimension != attr_dimension + + fields = { + self_dimension: value[self_dimension] - self_offset, + attr_dimension: value[attr_dimension] - attr_offset, + } + return Vector(fields) + + def _mk_update_vector_center(self, value): + """ + Calculate the update vector for the midpoint of this side + """ + value = Vector(value) + # Pretty similar to ._mk_update_vector_side() + self_dimension, self_offset = self._lookup_side(self.side) + + attr_dimension = 'y' if self_dimension == 'x' else 'x' + + fields = { + self_dimension: value[self_dimension] - self_offset, + attr_dimension: value[attr_dimension] + } + + return Vector(fields) def _attribute_gate(self, attribute, bad_sides): if self.side in bad_sides: @@ -154,6 +189,10 @@ class Rotatable: def facing(self): return Vector(*self.basis).rotate(self.rotation).normalize() + @facing.setter + def facing(self, value): + self.rotation = self.basis.angle(value) + @property def rotation(self): return self._rotation @@ -187,9 +226,7 @@ class BaseSprite(EventMixin, Rotatable): def __init__(self, **kwargs): super().__init__() - # Make these instance properties with fresh instances - # Don't use Vector.convert() because we need copying - self.position = Vector(*self.position) + self.position = Vector(self.position) # Initialize things for k, v in kwargs.items(): @@ -198,7 +235,7 @@ def __init__(self, **kwargs): k = 'position' # Castings if k == 'position': - v = Vector(*v) # Vector.convert() when that ships. + v = Vector(v) setattr(self, k, v) # Trigger some calculations @@ -209,11 +246,8 @@ def center(self) -> Vector: return self.position @center.setter - def center(self, value: Sequence[float]): - x = value[0] - y = value[1] - self.position.x = x - self.position.y = y + def center(self, value: ppb_vector.vector2.VectorLike): + self.position = Vector(value) @property def left(self) -> Side: @@ -221,7 +255,7 @@ def left(self) -> Side: @left.setter def left(self, value: float): - self.position.x = value + self._offset_value + self.position = Vector(value + self._offset_value, self.position.y) @property def right(self) -> Side: @@ -229,7 +263,7 @@ def right(self) -> Side: @right.setter def right(self, value): - self.position.x = value - self._offset_value + self.position = Vector(value - self._offset_value, self.position.y) @property def top(self): @@ -237,7 +271,7 @@ def top(self): @top.setter def top(self, value): - self.position.y = value + self._offset_value + self.position = Vector(self.position.x, value + self._offset_value) @property def bottom(self): @@ -245,7 +279,7 @@ def bottom(self): @bottom.setter def bottom(self, value): - self.position.y = value - self._offset_value + self.position = Vector(self.position.x, value - self._offset_value) @property def _offset_value(self): diff --git a/requirements.txt b/requirements.txt index 0424c85e..bd6546a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ pygame -ppb-vector +ppb-vector >= 1.0a1, < 2 dataclasses; python_version < "3.7" diff --git a/tests/test_camera.py b/tests/test_camera.py index 6b29781e..b28bfec7 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -30,20 +30,20 @@ def test_camera_translate_to_frame(): cam = Camera(viewport=(0, 0, 800, 600), pixel_ratio=80) assert cam.position == Vector(0, 0) assert cam.translate_to_frame(Vector(400, 300)) == Vector(0, 0) - assert cam.translate_to_frame(Vector(560, 220)) == Vector(2, -1) + assert cam.translate_to_frame(Vector(560, 220)) == Vector(2, 1) cam.position = Vector(5, 5) - assert cam.translate_to_frame(Vector(400, 300)) == Vector(5, 5) - assert cam.translate_to_frame(Vector(560, 220)) == Vector(7, 4) + assert cam.translate_to_frame(Vector(400, 300)) == Vector(5, -5) + assert cam.translate_to_frame(Vector(560, 220)) == Vector(7, -4) def test_camera_translate_to_viewport(): cam = Camera(viewport=(0, 0, 800, 600), pixel_ratio=80) assert cam.position == Vector(0, 0) assert cam.translate_to_viewport(Vector(0, 0)) == Vector(400, 300) - assert cam.translate_to_viewport(Vector(2, -1)) == Vector(560, 220) + assert cam.translate_to_viewport(Vector(2, 1)) == Vector(560, 220) cam.position = Vector(5, 5) - assert cam.translate_to_viewport(Vector(5, 5)) == Vector(400, 300) - assert cam.translate_to_viewport(Vector(7, 4)) == Vector(560, 220) + assert cam.translate_to_viewport(Vector(5, -5)) == Vector(400, 300) + assert cam.translate_to_viewport(Vector(7, -4)) == Vector(560, 220) def test_sprite_in_viewport(): @@ -70,4 +70,4 @@ def test_viewport_change_affects_frame_height(): cam = Camera(viewport=(0, 0, 800, 600), pixel_ratio=80) assert cam.frame_left == -5 cam.viewport_width = 400 - assert cam.frame_left == -2.5 \ No newline at end of file + assert cam.frame_left == -2.5 diff --git a/tests/test_sprites.py b/tests/test_sprites.py index 4e766955..1a264f1f 100644 --- a/tests/test_sprites.py +++ b/tests/test_sprites.py @@ -82,15 +82,6 @@ def test_bottom(self): self.assertEqual(self.sprite.position.x, 0) self.assertEqual(self.sprite.position.y, 1.5) - def test_center_accessors(self): - self.sprite.center.x = 20 - self.assertEqual(self.sprite.position.x, 20) - self.assertEqual(self.sprite.position.y, 0) - - self.sprite.center.y = 15 - self.assertEqual(self.sprite.position.x, 20) - self.assertEqual(self.sprite.position.y, 15) - def test_left_top(self): self.assertEqual(self.sprite.left.top, Vector(-0.5, -0.5))