Skip to content

Add Circle contains() #2791

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

Merged
merged 7 commits into from
May 25, 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
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
54 changes: 54 additions & 0 deletions src_c/circle.c
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,59 @@ 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 *circle = &pgCircle_AsCircle(arg);
/*a circle is always contained within itself*/
if (circle == scirc)
Py_RETURN_TRUE;
/* a bigger circle can't be contained within a smaller circle */
if (circle->r > scirc->r)
Py_RETURN_FALSE;

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

result = (dx * dx + dy * dy) <= (dr * dr);
}
else if (pgRect_Check(arg)) {
SDL_Rect *rect = &pgRect_AsRect(arg);
RECT_CIRCLE_CONTAINS(rect, scirc, result);
}
else if (pgFRect_Check(arg)) {
SDL_FRect *frect = &pgFRect_AsRect(arg);
RECT_CIRCLE_CONTAINS(frect, 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 +448,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
145 changes: 142 additions & 3 deletions test/geometry_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import unittest
import math

import unittest
from math import sqrt
from pygame import Vector2, Vector3, Rect, FRect

from pygame import Vector2, Vector3, Rect, FRect
from pygame.geometry import Circle


Expand Down Expand Up @@ -1149,6 +1148,146 @@ 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)
items = [
Circle(3, 4, 15),
(0, 0),
Vector2(0, 0),
Rect(0, 0, 10, 10),
FRect(0, 0, 10, 10),
]

for item in items:
self.assertIsInstance(c.contains(item), 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))

# self-like
c_s = Circle(c)
self.assertTrue(c.contains(c_s))

# 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)
p4 = (c.x + math.sin(math.pi / 4) * c.r, c.y + math.cos(math.pi / 4) * c.r)

p1v = Vector2(p1)
p2v = Vector2(p2)
p3v = Vector2(p3)
p4v = Vector2(p4)

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

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

# on the edge
self.assertTrue(c.contains(p4))

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

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

# on the edge
self.assertTrue(c.contains(p4v))

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)

angle = math.pi / 4
x = c.x - math.sin(angle) * c.r
y = c.y - math.cos(angle) * c.r
rx = c.x + math.sin(angle) * c.r
ry = c.y + math.cos(angle) * c.r
r_edge = Rect(x, y, rx - x, ry - y)

fr1 = FRect(0, 0, 3, 3)
fr2 = FRect(10, 10, 10, 10)
fr3 = FRect(10, 10, 5, 5)
fr_edge = FRect(x, y, rx - x, ry - y)

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

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

# on the edge
self.assertTrue(c.contains(r_edge))

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

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

# on the edge
self.assertTrue(c.contains(fr_edge))


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