Skip to content

Commit

Permalink
Merge pull request #3071 from itzpr3d4t0r/circle-intersect
Browse files Browse the repository at this point in the history
Add `Circle.intersect()`
  • Loading branch information
ankith26 authored Sep 15, 2024
2 parents 72af0f0 + 6ed4d45 commit 15ee198
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 8 deletions.
3 changes: 3 additions & 0 deletions buildconfig/stubs/pygame/geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ from typing import (
Callable,
Protocol,
Tuple,
List,
)

from pygame import Rect, FRect
Expand All @@ -20,6 +21,7 @@ class _HasCirclettribute(Protocol):

_CircleValue = Union[_CanBeCircle, _HasCirclettribute]
_CanBeCollided = Union[Circle, Rect, FRect, Coordinate, Vector2]
_CanBeIntersected = Union[Circle]

class Circle:
@property
Expand Down Expand Up @@ -93,6 +95,7 @@ class Circle:
@overload
def colliderect(self, topleft: Coordinate, size: Coordinate, /) -> bool: ...
def collideswith(self, other: _CanBeCollided, /) -> bool: ...
def intersect(self, other: _CanBeIntersected, /) -> List[Tuple[float, float]]: ...
def contains(self, shape: _CanBeCollided) -> bool: ...
@overload
def update(self, circle: _CircleValue, /) -> None: ...
Expand Down
18 changes: 18 additions & 0 deletions docs/reST/ref/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,24 @@

.. ## Circle.move_ip ##
.. method:: intersect

| :sl:`finds intersections between the circle and a shape`
| :sg:`intersect(circle, /) -> list`
Finds and returns a list of intersection points between the circle and another shape.
The other shape must be a `Circle` object.
If the circle does not intersect or has infinite intersections, an empty list is returned.

.. note::
The shape argument must be an instance of the `Circle` class.
Passing a tuple or list of coordinates representing the shape is not supported,
as the type of shape cannot be determined from coordinates alone.

.. versionadded:: 2.5.2

.. ## Circle.intersect ##
.. method:: update

| :sl:`updates the circle position and radius`
Expand Down
32 changes: 24 additions & 8 deletions src_c/circle.c
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,28 @@ pg_circle_contains(pgCircleObject *self, PyObject *arg)
return PyBool_FromLong(result);
}

static PyObject *
pg_circle_intersect(pgCircleObject *self, PyObject *arg)
{
pgCircleBase *scirc = &self->circle;

/* max number of intersections when supporting: Circle (2), */
double intersections[4];
int num = 0;

if (pgCircle_Check(arg)) {
pgCircleBase *other = &pgCircle_AsCircle(arg);
num = pgIntersection_CircleCircle(scirc, other, intersections);
}
else {
PyErr_Format(PyExc_TypeError, "Argument must be a CircleType, got %s",
Py_TYPE(arg)->tp_name);
return NULL;
}

return pg_PointList_FromArrayDouble(intersections, num * 2);
}

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

#define GETTER_SETTER(name) \
Expand Down Expand Up @@ -643,14 +667,6 @@ pg_circle_setdiameter(pgCircleObject *self, PyObject *value, void *closure)
return 0;
}

static int
double_compare(double a, double b)
{
/* Uses both a fixed epsilon and an adaptive epsilon */
const double e = 1e-6;
return fabs(a - b) < e || fabs(a - b) <= e * MAX(fabs(a), fabs(b));
}

static PyObject *
pg_circle_richcompare(PyObject *self, PyObject *other, int op)
{
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 @@ -16,6 +16,7 @@
#define DOC_CIRCLE_CONTAINS "contains(circle, /) -> bool\ncontains(rect, /) -> bool\ncontains((x, y), /) -> bool\ncontains(vector2, /) -> bool\ntests if a shape or point is inside the circle"
#define DOC_CIRCLE_MOVE "move((x, y), /) -> Circle\nmove(x, y, /) -> Circle\nmove(vector2, /) -> Circle\nmoves the circle by a given amount"
#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_INTERSECT "intersect(circle, /) -> list\nfinds intersections between the circle and a shape"
#define DOC_CIRCLE_UPDATE "update((x, y), radius, /) -> None\nupdate(x, y, radius, /) -> None\nupdate(vector2, 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
8 changes: 8 additions & 0 deletions src_c/geometry_common.c
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,11 @@ pgCircle_FromObjectFastcall(PyObject *const *args, Py_ssize_t nargs,
return 0;
}
}

static inline int
double_compare(double a, double b)
{
/* Uses both a fixed epsilon and an adaptive epsilon */
const double e = 1e-6;
return fabs(a - b) < e || fabs(a - b) <= e * MAX(fabs(a), fabs(b));
}
48 changes: 48 additions & 0 deletions src_c/geometry_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ int
pgCircle_FromObjectFastcall(PyObject *const *args, Py_ssize_t nargs,
pgCircleBase *out);

static inline int
double_compare(double a, double b);

/* === Collision Functions === */

static inline int
Expand Down Expand Up @@ -49,4 +52,49 @@ pgCollision_RectCircle(double rx, double ry, double rw, double rh,
return pgCollision_CirclePoint(circle, test_x, test_y);
}

static inline int
pgIntersection_CircleCircle(pgCircleBase *A, pgCircleBase *B,
double *intersections)
{
double dx = B->x - A->x;
double dy = B->y - A->y;
double d2 = dx * dx + dy * dy;
double r_sum = A->r + B->r;
double r_diff = A->r - B->r;
double r_sum2 = r_sum * r_sum;
double r_diff2 = r_diff * r_diff;

if (d2 > r_sum2 || d2 < r_diff2) {
return 0;
}

if (double_compare(d2, 0) && double_compare(A->r, B->r)) {
return 0;
}

double d = sqrt(d2);
double a = (d2 + A->r * A->r - B->r * B->r) / (2 * d);
double h = sqrt(A->r * A->r - a * a);

double xm = A->x + a * (dx / d);
double ym = A->y + a * (dy / d);

double xs1 = xm + h * (dy / d);
double ys1 = ym - h * (dx / d);
double xs2 = xm - h * (dy / d);
double ys2 = ym + h * (dx / d);

if (double_compare(d2, r_sum2) || double_compare(d2, r_diff2)) {
intersections[0] = xs1;
intersections[1] = ys1;
return 1;
}

intersections[0] = xs1;
intersections[1] = ys1;
intersections[2] = xs2;
intersections[3] = ys2;
return 2;
}

#endif // PYGAME_CE_GEOMETRY_COMMON_H
29 changes: 29 additions & 0 deletions src_c/include/_pygame.h
Original file line number Diff line number Diff line change
Expand Up @@ -659,3 +659,32 @@ pg_tuple_couple_from_values_double(double val1, double val2)

return tuple;
}

static PG_INLINE PyObject *
pg_PointList_FromArrayDouble(double const *array, int arr_length)
{
if (arr_length % 2) {
return RAISE(PyExc_ValueError, "array length must be even");
}

int num_points = arr_length / 2;
PyObject *sequence = PyList_New(num_points);
if (!sequence) {
return NULL;
}

int i;
PyObject *point = NULL;
for (i = 0; i < num_points; i++) {
point =
pg_tuple_couple_from_values_double(array[i * 2], array[i * 2 + 1]);
if (!point) {
Py_DECREF(sequence);
return NULL;
}
PyList_SET_ITEM(sequence, i, point);
point = NULL;
}

return sequence;
}
54 changes: 54 additions & 0 deletions test/geometry_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,60 @@ def test_contains_rect_frect(self):
# on the edge
self.assertTrue(c.contains(fr_edge))

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

invalid_types = (None, "1", (1,), 1, (1, 2, 3), True, False)

c = Circle(10, 10, 4)

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

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

circles = [(Circle(10, 10, 4) for _ in range(100))]
for size in range(len(circles)):
with self.assertRaises(TypeError):
c.intersect(*circles[:size])

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

objects = [
Circle(10, 10, 4),
Circle(10, 10, 400),
Circle(10, 10, 1),
Circle(15, 10, 10),
]

for object in objects:
self.assertIsInstance(c.intersect(object), list)

def test_intersect(self):
# Circle
c = Circle(10, 10, 4)
c2 = Circle(10, 10, 2)
c3 = Circle(100, 100, 1)
c3_1 = Circle(10, 10, 400)
c4 = Circle(16, 10, 7)
c5 = Circle(18, 10, 4)

for circle in [c, c2, c3, c3_1]:
self.assertEqual(c.intersect(circle), [])

# intersecting circle
self.assertEqual(
[(10.25, 6.007820144332172), (10.25, 13.992179855667828)], c.intersect(c4)
)

# touching
self.assertEqual([(14.0, 10.0)], c.intersect(c5))


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

0 comments on commit 15ee198

Please sign in to comment.