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() #235

Merged
merged 7 commits into from
Jul 25, 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
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()
Loading