Skip to content

Commit

Permalink
Add Circle.intersect() (#235)
Browse files Browse the repository at this point in the history
* circle intersect

* fix test

* slightly faster formula

* better docs, added a test

* added a comment, fixed missing newline

* use more permissive comparisons

* fix docs a bit
  • Loading branch information
itzpr3d4t0r authored Jul 25, 2024
1 parent 2ddeef1 commit f984c3c
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 6 deletions.
18 changes: 17 additions & 1 deletion docs/circle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -404,4 +404,20 @@ Circle Methods
as the original `Circle` object. The function takes no arguments and returns the
new `Circle` object.

.. ## Circle.copy ##
.. ## Circle.copy ##
.. method:: intersect

| :sl:`finds intersections between the circle and another shape`
| :sg:`intersect(Circle) -> list[Tuple[float, float]]`
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.

.. ## Circle.intersect ##
2 changes: 2 additions & 0 deletions docs/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ performing transformations and checking for collisions with other objects.

as_rect: Returns the smallest rectangle that contains the circle.

intersect: Finds intersections between the circle and another shape.

Additionally to these, the circle shape can also be used as a collider for the ``geometry.raycast`` function.

Line
Expand Down
1 change: 1 addition & 0 deletions geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ class Circle:
def rotate_ip(
self, angle: float, rotation_point: Coordinate = Circle.center
) -> None: ...
def intersect(self, other: Circle) -> List[Tuple[float, float]]: ...

class Polygon:
vertices: List[Coordinate]
Expand Down
23 changes: 23 additions & 0 deletions src_c/circle.c
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,28 @@ pg_circle_collidelistall(pgCircleObject *self, PyObject *arg)
return ret;
}

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[] = {
{"collidecircle", (PyCFunction)pg_circle_collidecircle, METH_FASTCALL,
NULL},
Expand All @@ -752,6 +774,7 @@ static struct PyMethodDef pg_circle_methods[] = {
{"copy", (PyCFunction)pg_circle_copy, METH_NOARGS, NULL},
{"rotate", (PyCFunction)pg_circle_rotate, METH_FASTCALL, NULL},
{"rotate_ip", (PyCFunction)pg_circle_rotate_ip, METH_FASTCALL, NULL},
{"intersect", (PyCFunction)pg_circle_intersect, METH_O, NULL},
{NULL, NULL, 0, NULL}};

/* numeric functions */
Expand Down
45 changes: 45 additions & 0 deletions src_c/collisions.c
Original file line number Diff line number Diff line change
Expand Up @@ -588,3 +588,48 @@ pgRaycast_LineCircle(pgLineBase *line, pgCircleBase *circle, double max_t,

return 1;
}

static 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;
}
3 changes: 3 additions & 0 deletions src_c/include/collisions.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,8 @@ pgCollision_PolygonLine(pgPolygonBase *, pgLineBase *, int);
static int
pgCollision_CirclePolygon(pgCircleBase *, pgPolygonBase *, int);

static int
pgIntersection_CircleCircle(pgCircleBase *A, pgCircleBase *B,
double *intersections);

#endif /* ~_PG_COLLISIONS_H */
8 changes: 8 additions & 0 deletions src_c/include/geometry.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,12 @@ PG_FREEPOLY_COND(pgPolygonBase *poly, int was_sequence)
}
}

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));
}

#endif /* ~_GEOMETRY_H */
63 changes: 58 additions & 5 deletions test/test_circle.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import unittest

import math
import unittest
from math import sqrt

from pygame import Vector2, Vector3
from pygame import Rect

from geometry import Circle, Line, Polygon, regular_polygon
from pygame import Rect
from pygame import Vector2, Vector3

E_T = "Expected True, "
E_F = "Expected False, "
Expand Down Expand Up @@ -1482,6 +1480,61 @@ def test_collidelistall(self):
for objects, expected in zip([circles, rects, lines, polygons], expected):
self.assertEqual(c.collidelistall(objects), expected)

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 f984c3c

Please sign in to comment.