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.intersect() #3071

Merged
merged 6 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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()
Loading