Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Circle top, left, right, bottom attributes #237

Merged
merged 2 commits into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/circle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions docs/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
106 changes: 106 additions & 0 deletions src_c/circle.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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 */
Expand Down
204 changes: 204 additions & 0 deletions test/test_circle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading