Skip to content

Commit 3388ec9

Browse files
authored
Merge pull request #2791 from itzpr3d4t0r/circle-contains
Add Circle `contains()`
2 parents 5ce5eca + 5f32cb2 commit 3388ec9

File tree

5 files changed

+219
-3
lines changed

5 files changed

+219
-3
lines changed

buildconfig/stubs/pygame/geometry.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class Circle:
9494
@overload
9595
def colliderect(self, topleft: Coordinate, size: Coordinate, /) -> bool: ...
9696
def collideswith(self, other: _CanBeCollided, /) -> bool: ...
97+
def contains(self, shape: _CanBeCollided) -> bool: ...
9798
@overload
9899
def update(self, circle: _CircleValue, /) -> None: ...
99100
@overload

docs/reST/ref/geometry.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,27 @@
283283

284284
.. ## Circle.collideswith ##
285285
286+
.. method:: contains
287+
288+
| :sl:`check if a shape or point is inside the circle`
289+
| :sg:`contains(circle, /) -> bool`
290+
| :sg:`contains(rect, /) -> bool`
291+
| :sg:`contains((x, y), /) -> bool`
292+
| :sg:`contains(vector2, /) -> bool`
293+
294+
Checks whether a given shape or point is completely contained within the `Circle`.
295+
Takes a single argument which can be a `Circle`, `Rect`, `FRect`, or a point.
296+
Returns `True` if the shape or point is completely contained, and `False` otherwise.
297+
298+
.. note::
299+
The shape argument must be an actual shape object (`Circle`, `Rect`, or `FRect`).
300+
You can't pass a tuple or list of coordinates representing the shape (except for a point),
301+
because the shape type can't be determined from the coordinates alone.
302+
303+
.. versionadded:: 2.5.0
304+
305+
.. ## Circle.contains ##
306+
286307
.. method:: update
287308

288309
| :sl:`updates the circle position and radius`

src_c/circle.c

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,59 @@ pg_circle_as_frect(pgCircleObject *self, PyObject *_null)
371371
return pgFRect_New4((float)x, (float)y, (float)diameter, (float)diameter);
372372
}
373373

374+
#define RECT_CIRCLE_CONTAINS(rect, circle, result) \
375+
if (pgCollision_CirclePoint(scirc, (double)rect->x, (double)rect->y) && \
376+
pgCollision_CirclePoint(circle, (double)(rect->x + rect->w), \
377+
(double)rect->y) && \
378+
pgCollision_CirclePoint(circle, (double)rect->x, \
379+
(double)(rect->y + rect->h)) && \
380+
pgCollision_CirclePoint(circle, (double)(rect->x + rect->w), \
381+
(double)(rect->y + rect->h))) { \
382+
result = 1; \
383+
}
384+
385+
static PyObject *
386+
pg_circle_contains(pgCircleObject *self, PyObject *arg)
387+
{
388+
int result = 0;
389+
pgCircleBase *scirc = &self->circle;
390+
double x, y;
391+
392+
if (pgCircle_Check(arg)) {
393+
pgCircleBase *circle = &pgCircle_AsCircle(arg);
394+
/*a circle is always contained within itself*/
395+
if (circle == scirc)
396+
Py_RETURN_TRUE;
397+
/* a bigger circle can't be contained within a smaller circle */
398+
if (circle->r > scirc->r)
399+
Py_RETURN_FALSE;
400+
401+
const double dx = circle->x - scirc->x;
402+
const double dy = circle->y - scirc->y;
403+
const double dr = circle->r - scirc->r;
404+
405+
result = (dx * dx + dy * dy) <= (dr * dr);
406+
}
407+
else if (pgRect_Check(arg)) {
408+
SDL_Rect *rect = &pgRect_AsRect(arg);
409+
RECT_CIRCLE_CONTAINS(rect, scirc, result);
410+
}
411+
else if (pgFRect_Check(arg)) {
412+
SDL_FRect *frect = &pgFRect_AsRect(arg);
413+
RECT_CIRCLE_CONTAINS(frect, scirc, result);
414+
}
415+
else if (pg_TwoDoublesFromObj(arg, &x, &y)) {
416+
result = pgCollision_CirclePoint(scirc, x, y);
417+
}
418+
else {
419+
return RAISE(PyExc_TypeError,
420+
"Invalid shape argument, must be a Circle, Rect / Frect "
421+
"or a coordinate");
422+
}
423+
424+
return PyBool_FromLong(result);
425+
}
426+
374427
static struct PyMethodDef pg_circle_methods[] = {
375428
{"collidepoint", (PyCFunction)pg_circle_collidepoint, METH_FASTCALL,
376429
DOC_CIRCLE_COLLIDEPOINT},
@@ -395,6 +448,7 @@ static struct PyMethodDef pg_circle_methods[] = {
395448
DOC_CIRCLE_ROTATE},
396449
{"rotate_ip", (PyCFunction)pg_circle_rotate_ip, METH_FASTCALL,
397450
DOC_CIRCLE_ROTATEIP},
451+
{"contains", (PyCFunction)pg_circle_contains, METH_O, DOC_CIRCLE_CONTAINS},
398452
{NULL, NULL, 0, NULL}};
399453

400454
#define GETTER_SETTER(name) \

src_c/doc/geometry_doc.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#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"
1616
#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"
1717
#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"
18+
#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"
1819
#define DOC_CIRCLE_UPDATE "update((x, y), radius, /) -> None\nupdate(x, y, radius, /) -> None\nupdates the circle position and radius"
1920
#define DOC_CIRCLE_ROTATE "rotate(angle, rotation_point=Circle.center, /) -> Circle\nrotate(angle, /) -> Circle\nrotates the circle"
2021
#define DOC_CIRCLE_ROTATEIP "rotate_ip(angle, rotation_point=Circle.center, /) -> None\nrotate_ip(angle, /) -> None\nrotates the circle in place"

test/geometry_test.py

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import unittest
21
import math
3-
2+
import unittest
43
from math import sqrt
5-
from pygame import Vector2, Vector3, Rect, FRect
64

5+
from pygame import Vector2, Vector3, Rect, FRect
76
from pygame.geometry import Circle
87

98

@@ -1149,6 +1148,146 @@ def assert_approx_equal(circle1, circle2, eps=1e-12):
11491148
c.rotate_ip(angle, center)
11501149
assert_approx_equal(c, rotate_circle(c, angle, center))
11511150

1151+
def test_contains_argtype(self):
1152+
"""Tests if the function correctly handles incorrect types as parameters"""
1153+
1154+
invalid_types = (None, [], "1", (1,), 1, (1, 2, 3))
1155+
1156+
c = Circle(10, 10, 4)
1157+
1158+
for value in invalid_types:
1159+
with self.assertRaises(TypeError):
1160+
c.contains(value)
1161+
1162+
def test_contains_argnum(self):
1163+
"""Tests if the function correctly handles incorrect number of parameters"""
1164+
c = Circle(10, 10, 4)
1165+
1166+
invalid_args = [(Circle(10, 10, 4), Circle(10, 10, 4))]
1167+
1168+
with self.assertRaises(TypeError):
1169+
c.contains()
1170+
1171+
for arg in invalid_args:
1172+
with self.assertRaises(TypeError):
1173+
c.contains(*arg)
1174+
1175+
def test_contains_return_type(self):
1176+
"""Tests if the function returns the correct type"""
1177+
c = Circle(10, 10, 4)
1178+
items = [
1179+
Circle(3, 4, 15),
1180+
(0, 0),
1181+
Vector2(0, 0),
1182+
Rect(0, 0, 10, 10),
1183+
FRect(0, 0, 10, 10),
1184+
]
1185+
1186+
for item in items:
1187+
self.assertIsInstance(c.contains(item), bool)
1188+
1189+
def test_contains_circle(self):
1190+
"""Ensures that the contains method correctly determines if a circle is
1191+
contained within the circle"""
1192+
c = Circle(10, 10, 4)
1193+
c2 = Circle(10, 10, 2)
1194+
c3 = Circle(100, 100, 5)
1195+
c4 = Circle(16, 10, 7)
1196+
1197+
# self
1198+
self.assertTrue(c.contains(c))
1199+
1200+
# self-like
1201+
c_s = Circle(c)
1202+
self.assertTrue(c.contains(c_s))
1203+
1204+
# contained circle
1205+
self.assertTrue(c.contains(c2))
1206+
1207+
# not contained circle
1208+
self.assertFalse(c.contains(c3))
1209+
1210+
# intersecting circle
1211+
self.assertFalse(c.contains(c4))
1212+
1213+
# bigger circle not contained in smaller circle
1214+
self.assertFalse(c2.contains(c))
1215+
self.assertFalse(c3.contains(c4))
1216+
1217+
def test_contains_point(self):
1218+
"""Ensures that the contains method correctly determines if a point is
1219+
contained within the circle"""
1220+
c = Circle(10, 10, 4)
1221+
p1 = (10, 10)
1222+
p2 = (10, 15)
1223+
p3 = (100, 100)
1224+
p4 = (c.x + math.sin(math.pi / 4) * c.r, c.y + math.cos(math.pi / 4) * c.r)
1225+
1226+
p1v = Vector2(p1)
1227+
p2v = Vector2(p2)
1228+
p3v = Vector2(p3)
1229+
p4v = Vector2(p4)
1230+
1231+
# contained point
1232+
self.assertTrue(c.contains(p1))
1233+
1234+
# not contained point
1235+
self.assertFalse(c.contains(p2))
1236+
self.assertFalse(c.contains(p3))
1237+
1238+
# on the edge
1239+
self.assertTrue(c.contains(p4))
1240+
1241+
# contained point
1242+
self.assertTrue(c.contains(p1v))
1243+
1244+
# not contained point
1245+
self.assertFalse(c.contains(p2v))
1246+
self.assertFalse(c.contains(p3v))
1247+
1248+
# on the edge
1249+
self.assertTrue(c.contains(p4v))
1250+
1251+
def test_contains_rect_frect(self):
1252+
"""Ensures that the contains method correctly determines if a rect is
1253+
contained within the circle"""
1254+
c = Circle(0, 0, 10)
1255+
r1 = Rect(0, 0, 3, 3)
1256+
r2 = Rect(10, 10, 10, 10)
1257+
r3 = Rect(10, 10, 5, 5)
1258+
1259+
angle = math.pi / 4
1260+
x = c.x - math.sin(angle) * c.r
1261+
y = c.y - math.cos(angle) * c.r
1262+
rx = c.x + math.sin(angle) * c.r
1263+
ry = c.y + math.cos(angle) * c.r
1264+
r_edge = Rect(x, y, rx - x, ry - y)
1265+
1266+
fr1 = FRect(0, 0, 3, 3)
1267+
fr2 = FRect(10, 10, 10, 10)
1268+
fr3 = FRect(10, 10, 5, 5)
1269+
fr_edge = FRect(x, y, rx - x, ry - y)
1270+
1271+
# contained rect
1272+
self.assertTrue(c.contains(r1))
1273+
1274+
# not contained rect
1275+
self.assertFalse(c.contains(r2))
1276+
self.assertFalse(c.contains(r3))
1277+
1278+
# on the edge
1279+
self.assertTrue(c.contains(r_edge))
1280+
1281+
# contained rect
1282+
self.assertTrue(c.contains(fr1))
1283+
1284+
# not contained rect
1285+
self.assertFalse(c.contains(fr2))
1286+
self.assertFalse(c.contains(fr3))
1287+
1288+
# on the edge
1289+
self.assertTrue(c.contains(fr_edge))
1290+
11521291

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

0 commit comments

Comments
 (0)