diff --git a/buildconfig/stubs/pygame/geometry.pyi b/buildconfig/stubs/pygame/geometry.pyi index 14ab6563b0..20ed7c52b6 100644 --- a/buildconfig/stubs/pygame/geometry.pyi +++ b/buildconfig/stubs/pygame/geometry.pyi @@ -64,6 +64,22 @@ class Circle: def center(self) -> Tuple[float, float]: ... @center.setter def center(self, value: Coordinate) -> None: ... + @property + def top(self) -> Tuple[float, float]: ... + @top.setter + def top(self, value: Coordinate) -> None: ... + @property + def left(self) -> Tuple[float, float]: ... + @left.setter + def left(self, value: Coordinate) -> None: ... + @property + def bottom(self) -> Tuple[float, float]: ... + @bottom.setter + def bottom(self, value: Coordinate) -> None: ... + @property + def right(self) -> Tuple[float, float]: ... + @right.setter + def right(self, value: Coordinate) -> None: ... @overload def __init__(self, x: float, y: float, r: float) -> None: ... @overload diff --git a/docs/reST/ref/geometry.rst b/docs/reST/ref/geometry.rst index a7fc79e2f2..2a1d1b41d3 100644 --- a/docs/reST/ref/geometry.rst +++ b/docs/reST/ref/geometry.rst @@ -142,6 +142,58 @@ .. ## Circle.circumference ## + .. attribute:: top + + | :sl:`top coordinate of the circle` + | :sg:`top -> (float, float)` + + It's a tuple containing the `x` and `y` coordinates that represent the top + of the circle. + Reassigning it moves the circle to the new position. The radius will not be affected. + + .. versionadded:: 2.5.2 + + .. ## Circle.top ## + + .. attribute:: bottom + + | :sl:`bottom coordinate of the circle` + | :sg:`bottom -> (float, float)` + + It's a tuple containing the `x` and `y` coordinates that represent the bottom + of the circle. + Reassigning it moves the circle to the new position. The radius will not be affected. + + .. versionadded:: 2.5.2 + + .. ## Circle.bottom ## + + .. attribute:: left + + | :sl:`left coordinate of the circle` + | :sg:`left -> (float, float)` + + It's a tuple containing the `x` and `y` coordinates that represent the left + of the circle. + Reassigning it moves the circle to the new position. The radius will not be affected. + + .. versionadded:: 2.5.2 + + .. ## Circle.left ## + + .. attribute:: right + + | :sl:`right coordinate of the circle` + | :sg:`right -> (float, float)` + + It's a tuple containing the `x` and `y` coordinates that represent the right + of the circle. + Reassigning it moves the circle to the new position. The radius will not be affected. + + .. versionadded:: 2.5.2 + + .. ## Circle.right ## + **Circle Methods** ---- diff --git a/src_c/circle.c b/src_c/circle.c index 31afb1fb2c..d807910e52 100644 --- a/src_c/circle.c +++ b/src_c/circle.c @@ -667,6 +667,106 @@ pg_circle_setdiameter(pgCircleObject *self, PyObject *value, void *closure) return 0; } +static PyObject * +pg_circle_gettop(pgCircleObject *self, void *closure) +{ + return pg_tuple_couple_from_values_double(self->circle.x, + self->circle.y - self->circle.r); +} + +static int +pg_circle_settop(pgCircleObject *self, PyObject *value, void *closure) +{ + double x, y; + + DEL_ATTR_NOT_SUPPORTED_CHECK_NO_NAME(value); + + if (!pg_TwoDoublesFromObj(value, &x, &y)) { + PyErr_SetString(PyExc_TypeError, "Expected a sequence of 2 numbers"); + return -1; + } + + self->circle.y = y + self->circle.r; + self->circle.x = x; + + return 0; +} + +static PyObject * +pg_circle_getleft(pgCircleObject *self, void *closure) +{ + return pg_tuple_couple_from_values_double(self->circle.x - self->circle.r, + self->circle.y); +} + +static int +pg_circle_setleft(pgCircleObject *self, PyObject *value, void *closure) +{ + double x, y; + + DEL_ATTR_NOT_SUPPORTED_CHECK_NO_NAME(value); + + if (!pg_TwoDoublesFromObj(value, &x, &y)) { + PyErr_SetString(PyExc_TypeError, "Expected a sequence of 2 numbers"); + return -1; + } + + self->circle.x = x + self->circle.r; + self->circle.y = y; + + return 0; +} + +static PyObject * +pg_circle_getbottom(pgCircleObject *self, void *closure) +{ + return pg_tuple_couple_from_values_double(self->circle.x, + self->circle.y + self->circle.r); +} + +static int +pg_circle_setbottom(pgCircleObject *self, PyObject *value, void *closure) +{ + double x, y; + + DEL_ATTR_NOT_SUPPORTED_CHECK_NO_NAME(value); + + if (!pg_TwoDoublesFromObj(value, &x, &y)) { + PyErr_SetString(PyExc_TypeError, "Expected a sequence of 2 numbers"); + return -1; + } + + self->circle.y = y - self->circle.r; + self->circle.x = x; + + return 0; +} + +static PyObject * +pg_circle_getright(pgCircleObject *self, void *closure) +{ + return pg_tuple_couple_from_values_double(self->circle.x + self->circle.r, + self->circle.y); +} + +static int +pg_circle_setright(pgCircleObject *self, PyObject *value, void *closure) +{ + double x, y; + + DEL_ATTR_NOT_SUPPORTED_CHECK_NO_NAME(value); + + if (!pg_TwoDoublesFromObj(value, &x, &y)) { + PyErr_SetString(PyExc_TypeError, "Expected a sequence of 2 numbers"); + return -1; + } + + self->circle.x = x - self->circle.r; + self->circle.y = y; + + return 0; +} + static PyObject * pg_circle_richcompare(PyObject *self, PyObject *other, int op) { @@ -709,6 +809,14 @@ static PyGetSetDef pg_circle_getsets[] = { DOC_CIRCLE_AREA, NULL}, {"circumference", (getter)pg_circle_getcircumference, (setter)pg_circle_setcircumference, DOC_CIRCLE_CIRCUMFERENCE, NULL}, + {"top", (getter)pg_circle_gettop, (setter)pg_circle_settop, DOC_CIRCLE_TOP, + NULL}, + {"left", (getter)pg_circle_getleft, (setter)pg_circle_setleft, + DOC_CIRCLE_LEFT, NULL}, + {"bottom", (getter)pg_circle_getbottom, (setter)pg_circle_setbottom, + DOC_CIRCLE_BOTTOM, NULL}, + {"right", (getter)pg_circle_getright, (setter)pg_circle_setright, + DOC_CIRCLE_RIGHT, NULL}, {NULL, 0, NULL, NULL, NULL}}; static PyTypeObject pgCircle_Type = { diff --git a/src_c/doc/geometry_doc.h b/src_c/doc/geometry_doc.h index 89a7ef6f3d..fca2f391bf 100644 --- a/src_c/doc/geometry_doc.h +++ b/src_c/doc/geometry_doc.h @@ -9,6 +9,10 @@ #define DOC_CIRCLE_DIAMETER "diameter -> float\ndiameter of the circle" #define DOC_CIRCLE_AREA "area -> float\narea of the circle" #define DOC_CIRCLE_CIRCUMFERENCE "circumference -> float\ncircumference of the circle" +#define DOC_CIRCLE_TOP "top -> (float, float)\ntop coordinate of the circle" +#define DOC_CIRCLE_BOTTOM "bottom -> (float, float)\nbottom coordinate of the circle" +#define DOC_CIRCLE_LEFT "left -> (float, float)\nleft coordinate of the circle" +#define DOC_CIRCLE_RIGHT "right -> (float, float)\nright coordinate of the circle" #define DOC_CIRCLE_COLLIDEPOINT "collidepoint((x, y), /) -> bool\ncollidepoint(x, y, /) -> bool\ncollidepoint(vector2, /) -> bool\ntests if a point is inside the circle" #define DOC_CIRCLE_COLLIDECIRCLE "collidecircle(circle, /) -> bool\ncollidecircle(x, y, radius, /) -> bool\ncollidecircle((x, y), radius, /) -> bool\ncollidecircle(vector2, radius, /) -> bool\ntests if a circle collides with this circle" #define DOC_CIRCLE_COLLIDERECT "colliderect(rect, /) -> bool\ncolliderect((x, y, width, height), /) -> bool\ncolliderect(x, y, width, height, /) -> bool\ncolliderect((x, y), (width, height), /) -> bool\ncolliderect(vector2, (width, height), /) -> bool\ntests if a rectangle collides with this circle" diff --git a/test/geometry_test.py b/test/geometry_test.py index e95d43ba46..c6fc049fe4 100644 --- a/test/geometry_test.py +++ b/test/geometry_test.py @@ -300,6 +300,210 @@ def test_center_del(self): with self.assertRaises(AttributeError): del c.center + def test_top(self): + """Ensures changing the top attribute moves the circle and does not change the circle's radius.""" + expected_radius = 5.0 + + for pos in [ + (1, 0), + (0, 0), + (-1, 0), + (0, -1), + (1, 1), + (-1, -1), + (-1, 1), + (1, -1), + ]: + c = Circle((0, 0), expected_radius) + + c.top = pos + + self.assertEqual(pos[0], c.x) + self.assertEqual(pos[1], c.y - expected_radius) + + self.assertEqual(expected_radius, c.r) + + def test_top_update(self): + """Ensures changing the x or y value of the circle correctly updates the top.""" + expected_x = 10.3 + expected_y = 2.12 + expected_radius = 5.0 + c = Circle(1, 1, expected_radius) + + c.x = expected_x + self.assertEqual(c.top, (expected_x, c.y - expected_radius)) + + c.y = expected_y + self.assertEqual(c.top, (c.x, expected_y - expected_radius)) + + def test_top_invalid_value(self): + """Ensures the top attribute handles invalid values correctly.""" + c = Circle(0, 0, 1) + + for value in (None, [], "1", (1,), [1, 2, 3], True, False): + with self.assertRaises(TypeError): + c.top = value + + def test_top_del(self): + """Ensures the top attribute can't be deleted.""" + c = Circle(0, 0, 1) + + with self.assertRaises(AttributeError): + del c.top + + def test_left(self): + """Ensures changing the left attribute moves the circle and does not change the circle's radius.""" + expected_radius = 5.0 + + for pos in [ + (1, 0), + (0, 0), + (-1, 0), + (0, -1), + (1, 1), + (-1, -1), + (-1, 1), + (1, -1), + ]: + c = Circle((0, 0), expected_radius) + + c.left = pos + + self.assertEqual(pos[0], c.x - expected_radius) + self.assertEqual(pos[1], c.y) + + self.assertEqual(expected_radius, c.r) + + def test_left_update(self): + """Ensures changing the x or y value of the circle correctly updates the left.""" + expected_x = 10.3 + expected_y = 2.12 + expected_radius = 5.0 + c = Circle(1, 1, expected_radius) + + c.x = expected_x + self.assertEqual(c.left, (expected_x - expected_radius, c.y)) + + c.y = expected_y + self.assertEqual(c.left, (c.x - expected_radius, expected_y)) + + def test_left_invalid_value(self): + """Ensures the left attribute handles invalid values correctly.""" + c = Circle(0, 0, 1) + + for value in (None, [], "1", (1,), [1, 2, 3], True, False): + with self.assertRaises(TypeError): + c.left = value + + def test_left_del(self): + """Ensures the left attribute can't be deleted.""" + c = Circle(0, 0, 1) + + with self.assertRaises(AttributeError): + del c.left + + def test_right(self): + """Ensures changing the right attribute moves the circle and does not change the circle's radius.""" + expected_radius = 5.0 + + for pos in [ + (1, 0), + (0, 0), + (-1, 0), + (0, -1), + (1, 1), + (-1, -1), + (-1, 1), + (1, -1), + ]: + c = Circle((0, 0), expected_radius) + + c.right = pos + + self.assertEqual(pos[0], c.x + expected_radius) + self.assertEqual(pos[1], c.y) + + self.assertEqual(expected_radius, c.r) + + def test_right_update(self): + """Ensures changing the x or y value of the circle correctly updates the right.""" + expected_x = 10.3 + expected_y = 2.12 + expected_radius = 5.0 + c = Circle(1, 1, expected_radius) + + c.x = expected_x + self.assertEqual(c.right, (expected_x + expected_radius, c.y)) + + c.y = expected_y + self.assertEqual(c.right, (c.x + expected_radius, expected_y)) + + def test_right_invalid_value(self): + """Ensures the right attribute handles invalid values correctly.""" + c = Circle(0, 0, 1) + + for value in (None, [], "1", (1,), [1, 2, 3], True, False): + with self.assertRaises(TypeError): + c.right = value + + def test_right_del(self): + """Ensures the right attribute can't be deleted.""" + c = Circle(0, 0, 1) + + with self.assertRaises(AttributeError): + del c.right + + def test_bottom(self): + """Ensures changing the bottom attribute moves the circle and does not change the circle's radius.""" + expected_radius = 5.0 + + for pos in [ + (1, 0), + (0, 0), + (-1, 0), + (0, -1), + (1, 1), + (-1, -1), + (-1, 1), + (1, -1), + ]: + c = Circle((0, 0), expected_radius) + + c.bottom = pos + + self.assertEqual(pos[0], c.x) + self.assertEqual(pos[1], c.y + expected_radius) + + self.assertEqual(expected_radius, c.r) + + def test_bottom_update(self): + """Ensures changing the x or y value of the circle correctly updates the bottom.""" + expected_x = 10.3 + expected_y = 2.12 + expected_radius = 5.0 + c = Circle(1, 1, expected_radius) + + c.x = expected_x + self.assertEqual(c.bottom, (expected_x, c.y + expected_radius)) + + c.y = expected_y + self.assertEqual(c.bottom, (c.x, expected_y + expected_radius)) + + def test_bottom_invalid_value(self): + """Ensures the bottom attribute handles invalid values correctly.""" + c = Circle(0, 0, 1) + + for value in (None, [], "1", (1,), [1, 2, 3], True, False): + with self.assertRaises(TypeError): + c.bottom = value + + def test_bottom_del(self): + """Ensures the bottom attribute can't be deleted.""" + c = Circle(0, 0, 1) + + with self.assertRaises(AttributeError): + del c.bottom + def test_area(self): """Ensures the area is calculated correctly.""" c = Circle(0, 0, 1)