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 colliderect() #2560

Merged
merged 6 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 7 additions & 0 deletions buildconfig/stubs/pygame/geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ from typing import (
Sequence,
)

from pygame import Rect, FRect
from ._common import Coordinate

_CanBeCircle = Union[Circle, Tuple[Coordinate, float], Sequence[float]]
Expand Down Expand Up @@ -75,5 +76,11 @@ class Circle:
def collidecircle(self, x: float, y: float, r: float) -> bool: ...
@overload
def collidecircle(self, center: Coordinate, r: float) -> bool: ...
@overload
def colliderect(self, rect: Union[Rect, FRect]) -> bool: ...
itzpr3d4t0r marked this conversation as resolved.
Show resolved Hide resolved
@overload
def colliderect(self, x: float, y: float, w: float, h: float) -> bool: ...
@overload
def colliderect(self, topleft: Coordinate, size: Coordinate) -> bool: ...
def __copy__(self) -> Circle: ...
copy = __copy__
13 changes: 13 additions & 0 deletions docs/reST/ref/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,19 @@

.. ## Circle.collidecircle ##

.. method:: colliderect

| :sl:`checks if a rectangle intersects the circle`
| :sg:`colliderect(Rect) -> bool`
| :sg:`colliderect((x, y, width, height)) -> bool`
| :sg:`colliderect(x, y, width, height) -> bool`
| :sg:`colliderect((x, y), (width, height)) -> bool`

The `colliderect` method tests whether a given rectangle intersects the `Circle`. It
takes either a `Rect` object, a tuple of (x, y, width, height) coordinates, or separate
x, y coordinates and width, height as its argument. Returns `True` if any portion
of the rectangle overlaps with the `Circle`, `False` otherwise.

.. method:: copy

| :sl:`returns a copy of the circle`
Expand Down
45 changes: 45 additions & 0 deletions src_c/circle.c
Original file line number Diff line number Diff line change
Expand Up @@ -290,11 +290,56 @@ pg_circle_collidecircle(pgCircleObject *self, PyObject *const *args,
pgCollision_CircleCircle(&self->circle, &other_circle));
}

static PyObject *
pg_circle_colliderect(pgCircleObject *self, PyObject *const *args,
Py_ssize_t nargs)
{
double x, y, w, h;

if (nargs == 1) {
SDL_FRect temp, *tmp;
if (!(tmp = pgFRect_FromObject(args[0], &temp))) {
return RAISE(PyExc_TypeError,
"Invalid rect, must be RectType or sequence of 4 "
"numbers");
}
x = (double)tmp->x;
y = (double)tmp->y;
w = (double)tmp->w;
h = (double)tmp->h;
}
else if (nargs == 2) {
if (!pg_TwoDoublesFromObj(args[0], &x, &y) ||
!pg_TwoDoublesFromObj(args[1], &w, &h)) {
return RAISE(PyExc_TypeError,
"Invalid rect, all 4 fields must be numeric");
}
}
else if (nargs == 4) {
if (!pg_DoubleFromObj(args[0], &x) || !pg_DoubleFromObj(args[1], &y) ||
!pg_DoubleFromObj(args[2], &w) || !pg_DoubleFromObj(args[3], &h)) {
return RAISE(PyExc_TypeError,
"Invalid rect, all 4 fields must be numeric");
}
}
else {
PyErr_Format(
PyExc_TypeError,
"Invalid number of arguments, expected 1, 2 or 4 (got %zd)",
nargs);
return NULL;
}
ankith26 marked this conversation as resolved.
Show resolved Hide resolved

return PyBool_FromLong(pgCollision_RectCircle(x, y, w, h, &self->circle));
}

static struct PyMethodDef pg_circle_methods[] = {
{"collidepoint", (PyCFunction)pg_circle_collidepoint, METH_FASTCALL,
DOC_CIRCLE_COLLIDEPOINT},
{"collidecircle", (PyCFunction)pg_circle_collidecircle, METH_FASTCALL,
DOC_CIRCLE_COLLIDECIRCLE},
{"colliderect", (PyCFunction)pg_circle_colliderect, METH_FASTCALL,
DOC_CIRCLE_COLLIDERECT},
{"__copy__", (PyCFunction)pg_circle_copy, METH_NOARGS, DOC_CIRCLE_COPY},
{"copy", (PyCFunction)pg_circle_copy, METH_NOARGS, DOC_CIRCLE_COPY},
{NULL, NULL, 0, NULL}};
Expand Down
17 changes: 15 additions & 2 deletions src_c/collisions.c
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#include "collisions.h"

static int
static inline int
pgCollision_CirclePoint(pgCircleBase *circle, double Cx, double Cy)
{
double dx = circle->x - Cx;
double dy = circle->y - Cy;
return dx * dx + dy * dy <= circle->r * circle->r;
}

static int
static inline int
ankith26 marked this conversation as resolved.
Show resolved Hide resolved
pgCollision_CircleCircle(pgCircleBase *A, pgCircleBase *B)
{
double dx, dy;
Expand All @@ -20,3 +20,16 @@ pgCollision_CircleCircle(pgCircleBase *A, pgCircleBase *B)

return dx * dx + dy * dy <= sum_radi * sum_radi;
}

static inline int
pgCollision_RectCircle(double rx, double ry, double rw, double rh,
pgCircleBase *circle)
{
const double cx = circle->x, cy = circle->y;
const double r_bottom = ry + rh, r_right = rx + rw;

const double test_x = (cx < rx) ? rx : ((cx > r_right) ? r_right : cx);
const double test_y = (cy < ry) ? ry : ((cy > r_bottom) ? r_bottom : cy);

return pgCollision_CirclePoint(circle, test_x, test_y);
}
8 changes: 6 additions & 2 deletions src_c/collisions.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@

#include "geometry.h"

static int
static inline int
pgCollision_CirclePoint(pgCircleBase *circle, double, double);

static int
static inline int
pgCollision_CircleCircle(pgCircleBase *, pgCircleBase *);

static inline int
pgCollision_RectCircle(double rx, double ry, double rw, double rh,
pgCircleBase *circle);

#endif /* ~_PG_COLLISIONS_H */
1 change: 1 addition & 0 deletions src_c/doc/geometry_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
#define DOC_CIRCLE_CIRCUMFERENCE "circumference -> float\ncircumference of the circle"
#define DOC_CIRCLE_COLLIDEPOINT "collidepoint((x, y)) -> bool\ncollidepoint(x, y) -> bool\ncollidepoint(Vector2) -> bool\ntest if a point is inside the circle"
#define DOC_CIRCLE_COLLIDECIRCLE "collidecircle(Circle) -> bool\ncollidecircle(x, y, radius) -> bool\ncollidecircle((x, y), radius) -> bool\ntest if two circles collide"
#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_COPY "copy() -> Circle\nreturns a copy of the circle"
5 changes: 5 additions & 0 deletions src_c/geometry.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ MODINIT_DEFINE(geometry)
return NULL;
}

import_pygame_rect();
if (PyErr_Occurred()) {
return NULL;
}

if (PyType_Ready(&pgCircle_Type) < 0) {
return NULL;
}
Expand Down
69 changes: 68 additions & 1 deletion test/geometry_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import math

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

from pygame.geometry import Circle

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

def test_colliderect_argtype(self):
"""tests if the function correctly handles incorrect types as parameters"""
invalid_types = (None, [], "1", (1,), Vector3(1, 1, 1), 1, True, False)

c = Circle(10, 10, 4)

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

def test_colliderect_argnum(self):
"""tests if the function correctly handles incorrect number of parameters"""
c = Circle(10, 10, 4)
args = [(1), (1, 1), (1, 1, 1), (1, 1, 1, 1, 1)]
# no params
with self.assertRaises(TypeError):
c.colliderect()

# invalid num
for arg in args:
with self.assertRaises(TypeError):
c.colliderect(*arg)

def test_colliderect(self):
"""ensures the function correctly detects collisions with rects"""

msgt = "Expected True, rect should collide here"
msgf = "Expected False, rect should not collide here"
# ====================================================
c = Circle(0, 0, 5)

r1, r2, r3 = Rect(2, 2, 4, 4), Rect(10, 15, 43, 24), Rect(0, 5, 4, 4)
fr1, fr2, fr3 = FRect(r1), FRect(r2), FRect(r3)

# colliding single
for r in (r1, fr1):
self.assertTrue(c.colliderect(r), msgt)

# not colliding single
for r in (r2, fr2):
self.assertFalse(c.colliderect(r), msgf)

# barely colliding single
for r in (r3, fr3):
self.assertTrue(c.colliderect(r), msgt)

# colliding 4 args
self.assertTrue(c.colliderect(2, 2, 4, 4), msgt)

# not colliding 4 args
self.assertFalse(c.colliderect(10, 15, 43, 24), msgf)

# barely colliding single
self.assertTrue(c.colliderect(0, 4.9999999999999, 4, 4), msgt)

# ensure FRects aren't truncated
c2 = Circle(0, 0, 0.35)
c3 = Circle(2, 0, 0.65)
fr9 = FRect(0.4, 0.0, 1, 1)
self.assertFalse(c2.colliderect(fr9), msgf)
self.assertFalse(c2.colliderect(0.4, 0.0, 1, 1), msgf)
self.assertFalse(c2.colliderect((0.4, 0.0), (1, 1)), msgf)

self.assertTrue(c3.colliderect(fr9), msgt)
self.assertTrue(c3.colliderect(0.4, 0.0, 1, 1), msgt)
self.assertTrue(c3.colliderect((0.4, 0.0), (1, 1)), msgt)


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