Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions buildconfig/stubs/pygame/geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ class Circle:
@overload
def colliderect(self, topleft: Coordinate, size: Coordinate, /) -> bool: ...
def collideswith(self, other: _CanBeCollided, /) -> bool: ...
def contains(self, shape: _CanBeCollided) -> bool: ...
@overload
def update(self, circle: _CircleValue, /) -> None: ...
@overload
Expand Down
21 changes: 21 additions & 0 deletions docs/reST/ref/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,27 @@

.. ## Circle.collideswith ##

.. method:: contains

| :sl:`check if a shape or point is inside the circle`
| :sg:`contains(circle, /) -> bool`
| :sg:`contains(rect, /) -> bool`
| :sg:`contains((x, y), /) -> bool`
| :sg:`contains(vector2, /) -> bool`

Checks whether a given shape or point is completely contained within the `Circle`.
Takes a single argument which can be a `Circle`, `Rect`, `FRect`, or a point.
Returns `True` if the shape or point is completely contained, and `False` otherwise.

.. note::
The shape argument must be an actual shape object (`Circle`, `Rect`, or `FRect`).
You can't pass a tuple or list of coordinates representing the shape (except for a point),
because the shape type can't be determined from the coordinates alone.

.. versionadded:: 2.5.0

.. ## Circle.contains ##

.. method:: update

| :sl:`updates the circle position and radius`
Expand Down
56 changes: 56 additions & 0 deletions src_c/circle.c
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,61 @@ pg_circle_as_frect(pgCircleObject *self, PyObject *_null)
return pgFRect_New4((float)x, (float)y, (float)diameter, (float)diameter);
}

#define RECT_CIRCLE_CONTAINS(rect, circle, result) \
if (pgCollision_CirclePoint(scirc, (double)rect->x, (double)rect->y) && \
pgCollision_CirclePoint(circle, (double)(rect->x + rect->w), \
(double)rect->y) && \
pgCollision_CirclePoint(circle, (double)rect->x, \
(double)(rect->y + rect->h)) && \
pgCollision_CirclePoint(circle, (double)(rect->x + rect->w), \
(double)(rect->y + rect->h))) { \
result = 1; \
}

static PyObject *
pg_circle_contains(pgCircleObject *self, PyObject *arg)
{
int result = 0;
pgCircleBase *scirc = &self->circle;
double x, y;

if (pgCircle_Check(arg)) {
pgCircleBase *temp = &pgCircle_AsCircle(arg);
/*a circle is always contained within itself*/
if (temp == scirc)
Py_RETURN_TRUE;
/* a bigger circle can't be contained within a smaller circle */
if (temp->r > scirc->r)
Py_RETURN_FALSE;

const double dx = temp->x - scirc->x;
const double dy = temp->y - scirc->y;
const double dr = temp->r - scirc->r;

result = (dx * dx + dy * dy) <= (dr * dr);
}
else if (pgRect_Check(arg)) {
SDL_Rect *temp = &pgRect_AsRect(arg);

RECT_CIRCLE_CONTAINS(temp, scirc, result);
}
else if (pgFRect_Check(arg)) {
SDL_FRect *temp = &pgFRect_AsRect(arg);

RECT_CIRCLE_CONTAINS(temp, scirc, result);
}
else if (pg_TwoDoublesFromObj(arg, &x, &y)) {
result = pgCollision_CirclePoint(scirc, x, y);
}
else {
return RAISE(PyExc_TypeError,
"Invalid shape argument, must be a Circle, Rect / Frect "
"or a coordinate");
}

return PyBool_FromLong(result);
}

static struct PyMethodDef pg_circle_methods[] = {
{"collidepoint", (PyCFunction)pg_circle_collidepoint, METH_FASTCALL,
DOC_CIRCLE_COLLIDEPOINT},
Expand All @@ -395,6 +450,7 @@ static struct PyMethodDef pg_circle_methods[] = {
DOC_CIRCLE_ROTATE},
{"rotate_ip", (PyCFunction)pg_circle_rotate_ip, METH_FASTCALL,
DOC_CIRCLE_ROTATEIP},
{"contains", (PyCFunction)pg_circle_contains, METH_O, DOC_CIRCLE_CONTAINS},
{NULL, NULL, 0, NULL}};

#define GETTER_SETTER(name) \
Expand Down
1 change: 1 addition & 0 deletions src_c/doc/geometry_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#define DOC_CIRCLE_MOVEIP "move_ip((x, y), /) -> None\nmove_ip(x, y, /) -> None\nmove_ip(vector2, /) -> None\nmoves the circle by a given amount, in place"
#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\nchecks if a rectangle intersects the circle"
#define DOC_CIRCLE_COLLIDESWITH "collideswith(circle, /) -> bool\ncollideswith(rect, /) -> bool\ncollideswith((x, y), /) -> bool\ncollideswith(vector2, /) -> bool\ncheck if a shape or point collides with the circle"
#define DOC_CIRCLE_CONTAINS "contains(circle, /) -> bool\ncontains(rect, /) -> bool\ncontains((x, y), /) -> bool\ncontains(vector2, /) -> bool\ncheck if a shape or point is inside the circle"
#define DOC_CIRCLE_UPDATE "update((x, y), radius, /) -> None\nupdate(x, y, radius, /) -> None\nupdates the circle position and radius"
#define DOC_CIRCLE_ROTATE "rotate(angle, rotation_point=Circle.center, /) -> Circle\nrotate(angle, /) -> Circle\nrotates the circle"
#define DOC_CIRCLE_ROTATEIP "rotate_ip(angle, rotation_point=Circle.center, /) -> None\nrotate_ip(angle, /) -> None\nrotates the circle in place"
Expand Down
106 changes: 106 additions & 0 deletions test/geometry_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,112 @@ def assert_approx_equal(circle1, circle2, eps=1e-12):
c.rotate_ip(angle, center)
assert_approx_equal(c, rotate_circle(c, angle, center))

def test_contains_argtype(self):
"""Tests if the function correctly handles incorrect types as parameters"""

invalid_types = (None, [], "1", (1,), 1, (1, 2, 3))

c = Circle(10, 10, 4)

for value in invalid_types:
with self.assertRaises(TypeError):
c.contains(value)

def test_contains_argnum(self):
"""Tests if the function correctly handles incorrect number of parameters"""
c = Circle(10, 10, 4)

invalid_args = [(Circle(10, 10, 4), Circle(10, 10, 4))]

with self.assertRaises(TypeError):
c.contains()

for arg in invalid_args:
with self.assertRaises(TypeError):
c.contains(*arg)

def test_contains_return_type(self):
"""Tests if the function returns the correct type"""
c = Circle(10, 10, 4)

self.assertIsInstance(c.contains(Circle(10, 10, 4)), bool)

def test_contains_circle(self):
"""Ensures that the contains method correctly determines if a circle is
contained within the circle"""
c = Circle(10, 10, 4)
c2 = Circle(10, 10, 2)
c3 = Circle(100, 100, 5)
c4 = Circle(16, 10, 7)

# self
self.assertTrue(c.contains(c))

# contained circle
self.assertTrue(c.contains(c2))

# not contained circle
self.assertFalse(c.contains(c3))

# intersecting circle
self.assertFalse(c.contains(c4))

# bigger circle not contained in smaller circle
self.assertFalse(c2.contains(c))
self.assertFalse(c3.contains(c4))

def test_contains_point(self):
"""Ensures that the contains method correctly determines if a point is
contained within the circle"""
c = Circle(10, 10, 4)
p1 = (10, 10)
p2 = (10, 15)
p3 = (100, 100)

p1v = Vector2(10, 10)
p2v = Vector2(10, 15)
p3v = Vector2(100, 100)

# contained point
self.assertTrue(c.contains(p1))

# not contained point
self.assertFalse(c.contains(p2))
self.assertFalse(c.contains(p3))

# contained point
self.assertTrue(c.contains(p1v))

# not contained point
self.assertFalse(c.contains(p2v))
self.assertFalse(c.contains(p3v))

def test_contains_rect_frect(self):
"""Ensures that the contains method correctly determines if a rect is
contained within the circle"""
c = Circle(0, 0, 10)
r1 = Rect(0, 0, 3, 3)
r2 = Rect(10, 10, 10, 10)
r3 = Rect(10, 10, 5, 5)

fr1 = FRect(0, 0, 3, 3)
fr2 = FRect(10, 10, 10, 10)
fr3 = FRect(10, 10, 5, 5)

# contained rect
self.assertTrue(c.contains(r1))

# not contained rect
self.assertFalse(c.contains(r2))
self.assertFalse(c.contains(r3))

# contained rect
self.assertTrue(c.contains(fr1))

# not contained rect
self.assertFalse(c.contains(fr2))
self.assertFalse(c.contains(fr3))


if __name__ == "__main__":
unittest.main()