Skip to content

Commit 15ee198

Browse files
authored
Merge pull request #3071 from itzpr3d4t0r/circle-intersect
Add `Circle.intersect()`
2 parents 72af0f0 + 6ed4d45 commit 15ee198

File tree

8 files changed

+185
-8
lines changed

8 files changed

+185
-8
lines changed

buildconfig/stubs/pygame/geometry.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ from typing import (
44
Callable,
55
Protocol,
66
Tuple,
7+
List,
78
)
89

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

2122
_CircleValue = Union[_CanBeCircle, _HasCirclettribute]
2223
_CanBeCollided = Union[Circle, Rect, FRect, Coordinate, Vector2]
24+
_CanBeIntersected = Union[Circle]
2325

2426
class Circle:
2527
@property
@@ -93,6 +95,7 @@ class Circle:
9395
@overload
9496
def colliderect(self, topleft: Coordinate, size: Coordinate, /) -> bool: ...
9597
def collideswith(self, other: _CanBeCollided, /) -> bool: ...
98+
def intersect(self, other: _CanBeIntersected, /) -> List[Tuple[float, float]]: ...
9699
def contains(self, shape: _CanBeCollided) -> bool: ...
97100
@overload
98101
def update(self, circle: _CircleValue, /) -> None: ...

docs/reST/ref/geometry.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,24 @@
279279

280280
.. ## Circle.move_ip ##
281281
282+
.. method:: intersect
283+
284+
| :sl:`finds intersections between the circle and a shape`
285+
| :sg:`intersect(circle, /) -> list`
286+
287+
Finds and returns a list of intersection points between the circle and another shape.
288+
The other shape must be a `Circle` object.
289+
If the circle does not intersect or has infinite intersections, an empty list is returned.
290+
291+
.. note::
292+
The shape argument must be an instance of the `Circle` class.
293+
Passing a tuple or list of coordinates representing the shape is not supported,
294+
as the type of shape cannot be determined from coordinates alone.
295+
296+
.. versionadded:: 2.5.2
297+
298+
.. ## Circle.intersect ##
299+
282300
.. method:: update
283301

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

src_c/circle.c

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,28 @@ pg_circle_contains(pgCircleObject *self, PyObject *arg)
425425
return PyBool_FromLong(result);
426426
}
427427

428+
static PyObject *
429+
pg_circle_intersect(pgCircleObject *self, PyObject *arg)
430+
{
431+
pgCircleBase *scirc = &self->circle;
432+
433+
/* max number of intersections when supporting: Circle (2), */
434+
double intersections[4];
435+
int num = 0;
436+
437+
if (pgCircle_Check(arg)) {
438+
pgCircleBase *other = &pgCircle_AsCircle(arg);
439+
num = pgIntersection_CircleCircle(scirc, other, intersections);
440+
}
441+
else {
442+
PyErr_Format(PyExc_TypeError, "Argument must be a CircleType, got %s",
443+
Py_TYPE(arg)->tp_name);
444+
return NULL;
445+
}
446+
447+
return pg_PointList_FromArrayDouble(intersections, num * 2);
448+
}
449+
428450
static struct PyMethodDef pg_circle_methods[] = {
429451
{"collidepoint", (PyCFunction)pg_circle_collidepoint, METH_FASTCALL,
430452
DOC_CIRCLE_COLLIDEPOINT},
@@ -450,6 +472,8 @@ static struct PyMethodDef pg_circle_methods[] = {
450472
{"rotate_ip", (PyCFunction)pg_circle_rotate_ip, METH_FASTCALL,
451473
DOC_CIRCLE_ROTATEIP},
452474
{"contains", (PyCFunction)pg_circle_contains, METH_O, DOC_CIRCLE_CONTAINS},
475+
{"intersect", (PyCFunction)pg_circle_intersect, METH_O,
476+
DOC_CIRCLE_INTERSECT},
453477
{NULL, NULL, 0, NULL}};
454478

455479
#define GETTER_SETTER(name) \
@@ -643,14 +667,6 @@ pg_circle_setdiameter(pgCircleObject *self, PyObject *value, void *closure)
643667
return 0;
644668
}
645669

646-
static int
647-
double_compare(double a, double b)
648-
{
649-
/* Uses both a fixed epsilon and an adaptive epsilon */
650-
const double e = 1e-6;
651-
return fabs(a - b) < e || fabs(a - b) <= e * MAX(fabs(a), fabs(b));
652-
}
653-
654670
static PyObject *
655671
pg_circle_richcompare(PyObject *self, PyObject *other, int op)
656672
{

src_c/doc/geometry_doc.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#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"
1717
#define DOC_CIRCLE_MOVE "move((x, y), /) -> Circle\nmove(x, y, /) -> Circle\nmove(vector2, /) -> Circle\nmoves the circle by a given amount"
1818
#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"
19+
#define DOC_CIRCLE_INTERSECT "intersect(circle, /) -> list\nfinds intersections between the circle and a shape"
1920
#define DOC_CIRCLE_UPDATE "update((x, y), radius, /) -> None\nupdate(x, y, radius, /) -> None\nupdate(vector2, radius, /) -> None\nupdates the circle position and radius"
2021
#define DOC_CIRCLE_ROTATE "rotate(angle, rotation_point=Circle.center, /) -> Circle\nrotate(angle, /) -> Circle\nrotates the circle"
2122
#define DOC_CIRCLE_ROTATEIP "rotate_ip(angle, rotation_point=Circle.center, /) -> None\nrotate_ip(angle, /) -> None\nrotates the circle in place"

src_c/geometry_common.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,11 @@ pgCircle_FromObjectFastcall(PyObject *const *args, Py_ssize_t nargs,
146146
return 0;
147147
}
148148
}
149+
150+
static inline int
151+
double_compare(double a, double b)
152+
{
153+
/* Uses both a fixed epsilon and an adaptive epsilon */
154+
const double e = 1e-6;
155+
return fabs(a - b) < e || fabs(a - b) <= e * MAX(fabs(a), fabs(b));
156+
}

src_c/geometry_common.h

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ int
1313
pgCircle_FromObjectFastcall(PyObject *const *args, Py_ssize_t nargs,
1414
pgCircleBase *out);
1515

16+
static inline int
17+
double_compare(double a, double b);
18+
1619
/* === Collision Functions === */
1720

1821
static inline int
@@ -49,4 +52,49 @@ pgCollision_RectCircle(double rx, double ry, double rw, double rh,
4952
return pgCollision_CirclePoint(circle, test_x, test_y);
5053
}
5154

55+
static inline int
56+
pgIntersection_CircleCircle(pgCircleBase *A, pgCircleBase *B,
57+
double *intersections)
58+
{
59+
double dx = B->x - A->x;
60+
double dy = B->y - A->y;
61+
double d2 = dx * dx + dy * dy;
62+
double r_sum = A->r + B->r;
63+
double r_diff = A->r - B->r;
64+
double r_sum2 = r_sum * r_sum;
65+
double r_diff2 = r_diff * r_diff;
66+
67+
if (d2 > r_sum2 || d2 < r_diff2) {
68+
return 0;
69+
}
70+
71+
if (double_compare(d2, 0) && double_compare(A->r, B->r)) {
72+
return 0;
73+
}
74+
75+
double d = sqrt(d2);
76+
double a = (d2 + A->r * A->r - B->r * B->r) / (2 * d);
77+
double h = sqrt(A->r * A->r - a * a);
78+
79+
double xm = A->x + a * (dx / d);
80+
double ym = A->y + a * (dy / d);
81+
82+
double xs1 = xm + h * (dy / d);
83+
double ys1 = ym - h * (dx / d);
84+
double xs2 = xm - h * (dy / d);
85+
double ys2 = ym + h * (dx / d);
86+
87+
if (double_compare(d2, r_sum2) || double_compare(d2, r_diff2)) {
88+
intersections[0] = xs1;
89+
intersections[1] = ys1;
90+
return 1;
91+
}
92+
93+
intersections[0] = xs1;
94+
intersections[1] = ys1;
95+
intersections[2] = xs2;
96+
intersections[3] = ys2;
97+
return 2;
98+
}
99+
52100
#endif // PYGAME_CE_GEOMETRY_COMMON_H

src_c/include/_pygame.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,3 +659,32 @@ pg_tuple_couple_from_values_double(double val1, double val2)
659659

660660
return tuple;
661661
}
662+
663+
static PG_INLINE PyObject *
664+
pg_PointList_FromArrayDouble(double const *array, int arr_length)
665+
{
666+
if (arr_length % 2) {
667+
return RAISE(PyExc_ValueError, "array length must be even");
668+
}
669+
670+
int num_points = arr_length / 2;
671+
PyObject *sequence = PyList_New(num_points);
672+
if (!sequence) {
673+
return NULL;
674+
}
675+
676+
int i;
677+
PyObject *point = NULL;
678+
for (i = 0; i < num_points; i++) {
679+
point =
680+
pg_tuple_couple_from_values_double(array[i * 2], array[i * 2 + 1]);
681+
if (!point) {
682+
Py_DECREF(sequence);
683+
return NULL;
684+
}
685+
PyList_SET_ITEM(sequence, i, point);
686+
point = NULL;
687+
}
688+
689+
return sequence;
690+
}

test/geometry_test.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,6 +1295,60 @@ def test_contains_rect_frect(self):
12951295
# on the edge
12961296
self.assertTrue(c.contains(fr_edge))
12971297

1298+
def test_intersect_argtype(self):
1299+
"""Tests if the function correctly handles incorrect types as parameters"""
1300+
1301+
invalid_types = (None, "1", (1,), 1, (1, 2, 3), True, False)
1302+
1303+
c = Circle(10, 10, 4)
1304+
1305+
for value in invalid_types:
1306+
with self.assertRaises(TypeError):
1307+
c.intersect(value)
1308+
1309+
def test_intersect_argnum(self):
1310+
"""Tests if the function correctly handles incorrect number of parameters"""
1311+
c = Circle(10, 10, 4)
1312+
1313+
circles = [(Circle(10, 10, 4) for _ in range(100))]
1314+
for size in range(len(circles)):
1315+
with self.assertRaises(TypeError):
1316+
c.intersect(*circles[:size])
1317+
1318+
def test_intersect_return_type(self):
1319+
"""Tests if the function returns the correct type"""
1320+
c = Circle(10, 10, 4)
1321+
1322+
objects = [
1323+
Circle(10, 10, 4),
1324+
Circle(10, 10, 400),
1325+
Circle(10, 10, 1),
1326+
Circle(15, 10, 10),
1327+
]
1328+
1329+
for object in objects:
1330+
self.assertIsInstance(c.intersect(object), list)
1331+
1332+
def test_intersect(self):
1333+
# Circle
1334+
c = Circle(10, 10, 4)
1335+
c2 = Circle(10, 10, 2)
1336+
c3 = Circle(100, 100, 1)
1337+
c3_1 = Circle(10, 10, 400)
1338+
c4 = Circle(16, 10, 7)
1339+
c5 = Circle(18, 10, 4)
1340+
1341+
for circle in [c, c2, c3, c3_1]:
1342+
self.assertEqual(c.intersect(circle), [])
1343+
1344+
# intersecting circle
1345+
self.assertEqual(
1346+
[(10.25, 6.007820144332172), (10.25, 13.992179855667828)], c.intersect(c4)
1347+
)
1348+
1349+
# touching
1350+
self.assertEqual([(14.0, 10.0)], c.intersect(c5))
1351+
12981352

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

0 commit comments

Comments
 (0)