diff --git a/docs/circle.rst b/docs/circle.rst index 1ef173e9..38771d82 100644 --- a/docs/circle.rst +++ b/docs/circle.rst @@ -98,6 +98,39 @@ Circle Attributes It's calculated using the `circumference=2*pi*r` formula. It can be reassigned. If reassigned the circle radius will be changed to produce a circle with matching circumference. The circle will not be moved from its original position. + + .. 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. It can be reassigned. If reassigned, the circle will be moved + to the new position. The radius will not be affected. + + .. 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. It can be reassigned. If reassigned, the circle will be moved + to the new position. The radius will not be affected. + + .. 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. It can be reassigned. If reassigned, the circle will be moved + to the new position. The radius will not be affected. + + .. 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. It can be reassigned. If reassigned, the circle will be moved + to the new position. The radius will not be affected. + Circle Methods ------ The `Circle` functions which modify the position or size return a new copy of the diff --git a/docs/geometry.rst b/docs/geometry.rst index 4f061284..f4dfa45d 100644 --- a/docs/geometry.rst +++ b/docs/geometry.rst @@ -36,6 +36,14 @@ performing transformations and checking for collisions with other objects. circumference: The circumference of the circle. + top: The top point of the circle. + + bottom: The bottom point of the circle. + + left: The left point of the circle. + + right: The right point of the circle. + **Here is the full list of methods:** :: move: Moves the circle by the given amount. diff --git a/geometry.pyi b/geometry.pyi index fdd9fa48..38a2cb5f 100644 --- a/geometry.pyi +++ b/geometry.pyi @@ -148,6 +148,10 @@ class Circle: area: float circumference: float center: Tuple[float, float] + top: Tuple[float, float] + left: Tuple[float, float] + right: Tuple[float, float] + bottom: Tuple[float, float] __safe_for_unpickling__: Literal[True] __hash__: None # type: ignore diff --git a/src_c/circle.c b/src_c/circle.c index 8b9934e6..956c86d0 100644 --- a/src_c/circle.c +++ b/src_c/circle.c @@ -1026,6 +1026,106 @@ pg_circle_setdiameter(pgCircleObject *self, PyObject *value, void *closure) return 0; } +static PyObject * +pg_circle_gettop(pgCircleObject *self, void *closure) +{ + return pg_TupleFromDoublePair(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_TupleFromDoublePair(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_TupleFromDoublePair(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_TupleFromDoublePair(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_getsafepickle(pgCircleObject *self, void *closure) { @@ -1059,6 +1159,12 @@ static PyGetSetDef pg_circle_getsets[] = { {"area", (getter)pg_circle_getarea, (setter)pg_circle_setarea, NULL, NULL}, {"circumference", (getter)pg_circle_getcircumference, (setter)pg_circle_setcircumference, NULL, NULL}, + {"top", (getter)pg_circle_gettop, (setter)pg_circle_settop, NULL, NULL}, + {"left", (getter)pg_circle_getleft, (setter)pg_circle_setleft, NULL, NULL}, + {"bottom", (getter)pg_circle_getbottom, (setter)pg_circle_setbottom, NULL, + NULL}, + {"right", (getter)pg_circle_getright, (setter)pg_circle_setright, NULL, + NULL}, {"__safe_for_unpickling__", (getter)pg_circle_getsafepickle, NULL, NULL, NULL}, {NULL, 0, NULL, NULL, NULL} /* Sentinel */ diff --git a/test/test_circle.py b/test/test_circle.py index b05d198f..91246767 100644 --- a/test/test_circle.py +++ b/test/test_circle.py @@ -290,6 +290,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)