Skip to content

Commit da52aab

Browse files
itzpr3d4t0rEmc2356andrewhong04ScriptLineStudiosavaxar
authored
Add Circle colliderect() (#2560)
Co-authored-by: Emc2356 <63981925+emc2356@users.noreply.github.com> Co-authored-by: NovialRiptide <35881688+novialriptide@users.noreply.github.com> Co-authored-by: ScriptLineStudios <scriptlinestudios@protonmail.com> Co-authored-by: Avaxar <44055981+avaxar@users.noreply.github.com> Co-authored-by: maqa41 <amehebbet41@gmail.com>
1 parent 5d3e845 commit da52aab

File tree

8 files changed

+162
-6
lines changed

8 files changed

+162
-6
lines changed

buildconfig/stubs/pygame/geometry.pyi

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ from typing import (
77
Sequence,
88
)
99

10-
from ._common import Coordinate
10+
from ._common import Coordinate, RectValue
1111

1212
_CanBeCircle = Union[Circle, Tuple[Coordinate, float], Sequence[float]]
1313

@@ -76,6 +76,12 @@ class Circle:
7676
@overload
7777
def collidecircle(self, center: Coordinate, r: float) -> bool: ...
7878
@overload
79+
def colliderect(self, rect: RectValue) -> bool: ...
80+
@overload
81+
def colliderect(self, x: float, y: float, w: float, h: float) -> bool: ...
82+
@overload
83+
def colliderect(self, topleft: Coordinate, size: Coordinate) -> bool: ...
84+
@overload
7985
def update(self, circle: _CircleValue) -> None: ...
8086
@overload
8187
def update(self, x: float, y: float, r: float) -> None: ...

docs/reST/ref/geometry.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,21 @@
176176

177177
.. ## Circle.collidecircle ##
178178
179+
.. method:: colliderect
180+
181+
| :sl:`checks if a rectangle intersects the circle`
182+
| :sg:`colliderect(Rect) -> bool`
183+
| :sg:`colliderect((x, y, width, height)) -> bool`
184+
| :sg:`colliderect(x, y, width, height) -> bool`
185+
| :sg:`colliderect((x, y), (width, height)) -> bool`
186+
187+
The `colliderect` method tests whether a given rectangle intersects the `Circle`. It
188+
takes either a `Rect` object, a tuple of (x, y, width, height) coordinates, or separate
189+
x, y coordinates and width, height as its argument. Returns `True` if any portion
190+
of the rectangle overlaps with the `Circle`, `False` otherwise.
191+
192+
.. ## Circle.colliderect ##
193+
179194
.. method:: update
180195

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

src_c/circle.c

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,56 @@ pg_circle_collidecircle(pgCircleObject *self, PyObject *const *args,
302302
pgCollision_CircleCircle(&self->circle, &other_circle));
303303
}
304304

305+
static PyObject *
306+
pg_circle_colliderect(pgCircleObject *self, PyObject *const *args,
307+
Py_ssize_t nargs)
308+
{
309+
double x, y, w, h;
310+
311+
if (nargs == 1) {
312+
SDL_FRect temp, *tmp;
313+
if (!(tmp = pgFRect_FromObject(args[0], &temp))) {
314+
return RAISE(PyExc_TypeError,
315+
"Invalid rect, must be RectType or sequence of 4 "
316+
"numbers");
317+
}
318+
x = (double)tmp->x;
319+
y = (double)tmp->y;
320+
w = (double)tmp->w;
321+
h = (double)tmp->h;
322+
}
323+
else if (nargs == 2) {
324+
if (!pg_TwoDoublesFromObj(args[0], &x, &y) ||
325+
!pg_TwoDoublesFromObj(args[1], &w, &h)) {
326+
return RAISE(PyExc_TypeError,
327+
"Invalid rect, all 4 fields must be numeric");
328+
}
329+
}
330+
else if (nargs == 4) {
331+
if (!pg_DoubleFromObj(args[0], &x) || !pg_DoubleFromObj(args[1], &y) ||
332+
!pg_DoubleFromObj(args[2], &w) || !pg_DoubleFromObj(args[3], &h)) {
333+
return RAISE(PyExc_TypeError,
334+
"Invalid rect, all 4 fields must be numeric");
335+
}
336+
}
337+
else {
338+
PyErr_Format(
339+
PyExc_TypeError,
340+
"Invalid number of arguments, expected 1, 2 or 4 (got %zd)",
341+
nargs);
342+
return NULL;
343+
}
344+
345+
return PyBool_FromLong(pgCollision_RectCircle(x, y, w, h, &self->circle));
346+
}
347+
305348
static struct PyMethodDef pg_circle_methods[] = {
306349
{"collidepoint", (PyCFunction)pg_circle_collidepoint, METH_FASTCALL,
307350
DOC_CIRCLE_COLLIDEPOINT},
308351
{"collidecircle", (PyCFunction)pg_circle_collidecircle, METH_FASTCALL,
309352
DOC_CIRCLE_COLLIDECIRCLE},
353+
{"colliderect", (PyCFunction)pg_circle_colliderect, METH_FASTCALL,
354+
DOC_CIRCLE_COLLIDERECT},
310355
{"update", (PyCFunction)pg_circle_update, METH_FASTCALL,
311356
DOC_CIRCLE_UPDATE},
312357
{"__copy__", (PyCFunction)pg_circle_copy, METH_NOARGS, DOC_CIRCLE_COPY},

src_c/collisions.c

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
#include "collisions.h"
22

3-
static int
3+
static inline int
44
pgCollision_CirclePoint(pgCircleBase *circle, double Cx, double Cy)
55
{
66
double dx = circle->x - Cx;
77
double dy = circle->y - Cy;
88
return dx * dx + dy * dy <= circle->r * circle->r;
99
}
1010

11-
static int
11+
static inline int
1212
pgCollision_CircleCircle(pgCircleBase *A, pgCircleBase *B)
1313
{
1414
double dx, dy;
@@ -20,3 +20,16 @@ pgCollision_CircleCircle(pgCircleBase *A, pgCircleBase *B)
2020

2121
return dx * dx + dy * dy <= sum_radi * sum_radi;
2222
}
23+
24+
static inline int
25+
pgCollision_RectCircle(double rx, double ry, double rw, double rh,
26+
pgCircleBase *circle)
27+
{
28+
const double cx = circle->x, cy = circle->y;
29+
const double r_bottom = ry + rh, r_right = rx + rw;
30+
31+
const double test_x = (cx < rx) ? rx : ((cx > r_right) ? r_right : cx);
32+
const double test_y = (cy < ry) ? ry : ((cy > r_bottom) ? r_bottom : cy);
33+
34+
return pgCollision_CirclePoint(circle, test_x, test_y);
35+
}

src_c/collisions.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33

44
#include "geometry.h"
55

6-
static int
6+
static inline int
77
pgCollision_CirclePoint(pgCircleBase *circle, double, double);
88

9-
static int
9+
static inline int
1010
pgCollision_CircleCircle(pgCircleBase *, pgCircleBase *);
1111

12+
static inline int
13+
pgCollision_RectCircle(double rx, double ry, double rw, double rh,
14+
pgCircleBase *circle);
15+
1216
#endif /* ~_PG_COLLISIONS_H */

src_c/doc/geometry_doc.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@
1111
#define DOC_CIRCLE_CIRCUMFERENCE "circumference -> float\ncircumference of the circle"
1212
#define DOC_CIRCLE_COLLIDEPOINT "collidepoint((x, y)) -> bool\ncollidepoint(x, y) -> bool\ncollidepoint(Vector2) -> bool\ntest if a point is inside the circle"
1313
#define DOC_CIRCLE_COLLIDECIRCLE "collidecircle(Circle) -> bool\ncollidecircle(x, y, radius) -> bool\ncollidecircle((x, y), radius) -> bool\ntest if two circles collide"
14+
#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"
1415
#define DOC_CIRCLE_UPDATE "update((x, y), radius) -> None\nupdate(x, y, radius) -> None\nupdates the circle position and radius"
1516
#define DOC_CIRCLE_COPY "copy() -> Circle\nreturns a copy of the circle"

src_c/geometry.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ MODINIT_DEFINE(geometry)
2222
return NULL;
2323
}
2424

25+
import_pygame_rect();
26+
if (PyErr_Occurred()) {
27+
return NULL;
28+
}
29+
2530
if (PyType_Ready(&pgCircle_Type) < 0) {
2631
return NULL;
2732
}

test/geometry_test.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import math
33

44
from math import sqrt
5-
from pygame import Vector2, Vector3
5+
from pygame import Vector2, Vector3, Rect, FRect
66

77
from pygame.geometry import Circle
88

@@ -549,6 +549,73 @@ def test_collidecircle(self):
549549
c5.collidecircle(c), "Expected False, circles should collide here"
550550
)
551551

552+
def test_colliderect_argtype(self):
553+
"""tests if the function correctly handles incorrect types as parameters"""
554+
invalid_types = (None, [], "1", (1,), Vector3(1, 1, 1), 1, True, False)
555+
556+
c = Circle(10, 10, 4)
557+
558+
for value in invalid_types:
559+
with self.assertRaises(TypeError):
560+
c.colliderect(value)
561+
562+
def test_colliderect_argnum(self):
563+
"""tests if the function correctly handles incorrect number of parameters"""
564+
c = Circle(10, 10, 4)
565+
args = [(1), (1, 1), (1, 1, 1), (1, 1, 1, 1, 1)]
566+
# no params
567+
with self.assertRaises(TypeError):
568+
c.colliderect()
569+
570+
# invalid num
571+
for arg in args:
572+
with self.assertRaises(TypeError):
573+
c.colliderect(*arg)
574+
575+
def test_colliderect(self):
576+
"""ensures the function correctly detects collisions with rects"""
577+
578+
msgt = "Expected True, rect should collide here"
579+
msgf = "Expected False, rect should not collide here"
580+
# ====================================================
581+
c = Circle(0, 0, 5)
582+
583+
r1, r2, r3 = Rect(2, 2, 4, 4), Rect(10, 15, 43, 24), Rect(0, 5, 4, 4)
584+
fr1, fr2, fr3 = FRect(r1), FRect(r2), FRect(r3)
585+
586+
# colliding single
587+
for r in (r1, fr1):
588+
self.assertTrue(c.colliderect(r), msgt)
589+
590+
# not colliding single
591+
for r in (r2, fr2):
592+
self.assertFalse(c.colliderect(r), msgf)
593+
594+
# barely colliding single
595+
for r in (r3, fr3):
596+
self.assertTrue(c.colliderect(r), msgt)
597+
598+
# colliding 4 args
599+
self.assertTrue(c.colliderect(2, 2, 4, 4), msgt)
600+
601+
# not colliding 4 args
602+
self.assertFalse(c.colliderect(10, 15, 43, 24), msgf)
603+
604+
# barely colliding single
605+
self.assertTrue(c.colliderect(0, 4.9999999999999, 4, 4), msgt)
606+
607+
# ensure FRects aren't truncated
608+
c2 = Circle(0, 0, 0.35)
609+
c3 = Circle(2, 0, 0.65)
610+
fr9 = FRect(0.4, 0.0, 1, 1)
611+
self.assertFalse(c2.colliderect(fr9), msgf)
612+
self.assertFalse(c2.colliderect(0.4, 0.0, 1, 1), msgf)
613+
self.assertFalse(c2.colliderect((0.4, 0.0), (1, 1)), msgf)
614+
615+
self.assertTrue(c3.colliderect(fr9), msgt)
616+
self.assertTrue(c3.colliderect(0.4, 0.0, 1, 1), msgt)
617+
self.assertTrue(c3.colliderect((0.4, 0.0), (1, 1)), msgt)
618+
552619
def test_update(self):
553620
"""Ensures that updating the circle position
554621
and dimension correctly updates position and dimension"""

0 commit comments

Comments
 (0)