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

Polygon flip() / flip_ip() #221

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions docs/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ other objects.

rotate_ip: Rotates the polygon by the given amount in place.

flip: Flips the polygon along the given axes.

flip_ip: Flips the polygon along the given axes in place.

Functions
=========
The geometry module also contains a number of standalone functions for performing operations
Expand Down
36 changes: 35 additions & 1 deletion docs/polygon.rst
Original file line number Diff line number Diff line change
Expand Up @@ -411,4 +411,38 @@ Polygon Methods
Keep in mind that the more vertices the polygon has, the more CPU time it will
take to scale it.

.. ## Polygon.scale_ip ##
.. ## Polygon.scale_ip ##

.. method:: flip

| :sl:`flips the polygon`
| :sg:`flip(x, y, flip_around) -> Polygon`

Returns a new Polygon that is flipped horizontally and/or vertically. The original
Polygon is not modified. The flipping is done relative to the given point.
By default, the flipping is done relative to the center of the `Polygon`.

.. note::
If `x` is True, the Polygon will be flipped horizontally.
If `y` is True, the Polygon will be flipped vertically.
If `x` and `y` are both True, the Polygon will be flipped
horizontally and vertically.

.. ## Polygon.flip ##

.. method:: flip_ip

| :sl:`flips the polygon`
| :sg:`flip_ip(x, y, flip_around) -> None`

Flips the Polygon horizontally and/or vertically. The original Polygon is modified.
The flipping is done relative to the given point. By default, the flipping is done
relative to the center of the `Polygon`. Always returns None.

.. note::
If `x` is True, the Polygon will be flipped horizontally.
If `y` is True, the Polygon will be flipped vertically.
If `x` and `y` are both True, the Polygon will be flipped
horizontally and vertically.

.. ## Polygon.flip_ip ##
6 changes: 6 additions & 0 deletions geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ class Polygon:
def pop_vertex(self, index: int) -> Coordinate: ...
def scale(self, factor: float) -> Polygon: ...
def scale_ip(self, factor: float) -> None: ...
def flip(
self, x: bool, y: bool = False, flip_around: Coordinate = Polygon.center
) -> Polygon: ...
def flip_ip(
self, x: bool, y: bool = False, flip_around: Coordinate = Polygon.center
) -> None: ...

def regular_polygon(
sides: int, center: Coordinate, radius: float, angle: float = 0
Expand Down
75 changes: 75 additions & 0 deletions src_c/polygon.c
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,79 @@ pg_polygon_collidecircle(pgPolygonObject *self, PyObject *const *args,
pgCollision_CirclePolygon(&circle, &self->polygon, only_edges));
}

static void
pg_polygon_flip_helper(pgPolygonBase *poly, int dirx, int diry, double c_x,
double c_y)
{
Py_ssize_t i2, verts_num = poly->verts_num;
double *vertices = poly->vertices;

if (dirx && diry) {
for (i2 = 0; i2 < verts_num * 2; i2 += 2) {
vertices[i2] = c_x - (vertices[i2] - c_x);
vertices[i2 + 1] = c_y - (vertices[i2 + 1] - c_y);
}
return;
}

if (dirx) {
for (i2 = 0; i2 < verts_num * 2; i2 += 2) {
vertices[i2] = c_x - (vertices[i2] - c_x);
}
return;
}

for (i2 = 0; i2 < verts_num * 2; i2 += 2) {
vertices[i2 + 1] = c_y - (vertices[i2 + 1] - c_y);
}
}

#define FLIP_PREP \
pgPolygonBase *poly = &self->polygon; \
int dirx, diry = 0; \
double c_x = poly->centerx, c_y = poly->centery; \
if (!nargs || nargs > 3) { \
return RAISE( \
PyExc_TypeError, \
"Invalid number of arguments, expected 1, 2 or 3 arguments"); \
} \
dirx = PyObject_IsTrue(args[0]); \
if (nargs >= 2) { \
diry = PyObject_IsTrue(args[1]); \
} \
if (nargs == 3 && !pg_TwoDoublesFromObj(args[2], &c_x, &c_y)) { \
return RAISE(PyExc_TypeError, \
"Invalid flip point argument, must be a sequence " \
"of two numbers"); \
}

static PyObject *
pg_polygon_flip(pgPolygonObject *self, PyObject *const *args, Py_ssize_t nargs)
{
FLIP_PREP

pgPolygonObject *ret = _pg_polygon_subtype_new2_copy(Py_TYPE(self), poly);
if (!ret) {
return NULL;
}

pg_polygon_flip_helper(&ret->polygon, dirx, diry, c_x, c_y);

return (PyObject *)ret;
}

static PyObject *
pg_polygon_flip_ip(pgPolygonObject *self, PyObject *const *args,
Py_ssize_t nargs)
{
FLIP_PREP

pg_polygon_flip_helper(poly, dirx, diry, c_x, c_y);

Py_RETURN_NONE;
}
#undef FLIP_PREP

static struct PyMethodDef pg_polygon_methods[] = {
{"as_segments", (PyCFunction)pg_polygon_as_segments, METH_NOARGS, NULL},
{"move", (PyCFunction)pg_polygon_move, METH_FASTCALL, NULL},
Expand All @@ -1323,6 +1396,8 @@ static struct PyMethodDef pg_polygon_methods[] = {
{"pop_vertex", (PyCFunction)pg_polygon_pop_vertex, METH_O, NULL},
{"scale", (PyCFunction)pg_polygon_scale, METH_O, NULL},
{"scale_ip", (PyCFunction)pg_polygon_scale_ip, METH_O, NULL},
{"flip", (PyCFunction)pg_polygon_flip, METH_FASTCALL, NULL},
{"flip_ip", (PyCFunction)pg_polygon_flip_ip, METH_FASTCALL, NULL},
{NULL, NULL, 0, NULL}};

static PyObject *
Expand Down
142 changes: 142 additions & 0 deletions test/test_polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ def _scale_polygon(vertices, num_verts, cx, cy, fac):
return new_vertices


def _flip_polygon(polygon, flip_x, flip_y=False, flip_center=None):
flipped_vertices = []

f_x, f_y = flip_center if flip_center is not None else polygon.center

for vertex in polygon.vertices:
new_x = vertex[0] if not flip_x else f_x - (vertex[0] - f_x)
new_y = vertex[1] if not flip_y else f_y - (vertex[1] - f_y)

flipped_vertices.append((new_x, new_y))

return flipped_vertices


class PolygonTypeTest(unittest.TestCase):
def test_Construction_invalid_type(self):
"""Checks whether passing wrong types to the constructor
Expand Down Expand Up @@ -2178,6 +2192,134 @@ def test_collidepolygon(self):
# line touches polygon vertex
self.assertTrue(l.collidepolygon(p5, True))

def test_flip_argnum(self):
"""Tests whether the function can handle invalid parameter number correctly."""
poly = Polygon(_some_vertices.copy())

invalid_args = [(1, 0, 1, 0), (1, 0, 1, 0, 1), (1, 0, 1, 0, 1, 1)]

with self.assertRaises(TypeError):
poly.flip()

for arg in invalid_args:
with self.assertRaises(TypeError):
poly.flip(*arg)

def test_flip_return_type(self):
"""Tests whether the flip method returns the correct type."""
poly = Polygon(_some_vertices.copy())

self.assertIsInstance(poly.flip(True), Polygon)
self.assertIsInstance(poly.flip(True, False), Polygon)
self.assertIsInstance(poly.flip(True, False, (10, 233)), Polygon)
self.assertIsInstance(poly.flip(True, False, (-10, -233)), Polygon)

def test_flip_ip_return_type(self):
"""Tests whether the flip_ip method returns the correct type."""
poly = Polygon(_some_vertices.copy())

self.assertIsInstance(poly.flip_ip(True), type(None))
self.assertIsInstance(poly.flip_ip(True, False), type(None))
self.assertIsInstance(poly.flip_ip(True, False, (10, 233)), type(None))
self.assertIsInstance(poly.flip_ip(True, False, (-10, -233)), type(None))

def test_flip_ip_argnum(self):
"""Tests whether the function can handle invalid parameter number correctly."""
poly = Polygon(_some_vertices.copy())

invalid_args = [(1, 0, 1, 0), (1, 0, 1, 0, 1), (1, 0, 1, 0, 1, 1)]

with self.assertRaises(TypeError):
poly.flip_ip()

for arg in invalid_args:
with self.assertRaises(TypeError):
poly.flip_ip(*arg)

def assert_vertices_equal(self, vertices1, vertices2, eps=1e-12):
self.assertEqual(len(vertices1), len(vertices2))

for v1, v2 in zip(vertices1, vertices2):
self.assertAlmostEqual(v1[0], v2[0], delta=eps)
self.assertAlmostEqual(v1[1], v2[1], delta=eps)

def test_flip(self):
"""Tests whether the flip method works correctly."""
poly = Polygon(_some_vertices.copy())

# x-axis
flipped_vertices = _flip_polygon(poly, True, False)
self.assert_vertices_equal(poly.flip(True).vertices, flipped_vertices)
self.assert_vertices_equal(poly.flip(True, False).vertices, flipped_vertices)

flipped_vertices = _flip_polygon(poly, True, False, (10, 233))
self.assert_vertices_equal(
poly.flip(True, False, (10, 233)).vertices, flipped_vertices
)

# y-axis
flipped_vertices = _flip_polygon(poly, False, True)
self.assert_vertices_equal(poly.flip(False, True).vertices, flipped_vertices)

flipped_vertices = _flip_polygon(poly, False, True, (10, 233))
self.assert_vertices_equal(
poly.flip(False, True, (10, 233)).vertices, flipped_vertices
)

# both axes
flipped_vertices = _flip_polygon(poly, True, True)
self.assert_vertices_equal(poly.flip(True, True).vertices, flipped_vertices)

flipped_vertices = _flip_polygon(poly, True, True, (10, 233))
self.assert_vertices_equal(
poly.flip(True, True, (10, 233)).vertices, flipped_vertices
)
flipped_vertices = _flip_polygon(poly, True, True, (-10, -233))
self.assert_vertices_equal(
poly.flip(True, True, (-10, -233)).vertices, flipped_vertices
)

def test_flip_ip(self):
"""Tests whether the flip_ip method works correctly."""
poly = Polygon(_some_vertices.copy())

# x-axis
flipped_vertices = _flip_polygon(poly, True, False)
poly.flip_ip(True)
self.assert_vertices_equal(poly.vertices, flipped_vertices)

poly = Polygon(_some_vertices.copy())
flipped_vertices = _flip_polygon(poly, True, False, (10, 233))
poly.flip_ip(True, False, (10, 233))
self.assert_vertices_equal(poly.vertices, flipped_vertices)

# y-axis
poly = Polygon(_some_vertices.copy())
flipped_vertices = _flip_polygon(poly, False, True)
poly.flip_ip(False, True)
self.assert_vertices_equal(poly.vertices, flipped_vertices)

poly = Polygon(_some_vertices.copy())
flipped_vertices = _flip_polygon(poly, False, True, (10, 233))
poly.flip_ip(False, True, (10, 233))
self.assert_vertices_equal(poly.vertices, flipped_vertices)

# both axes
poly = Polygon(_some_vertices.copy())
flipped_vertices = _flip_polygon(poly, True, True)
poly.flip_ip(True, True)
self.assert_vertices_equal(poly.vertices, flipped_vertices)

poly = Polygon(_some_vertices.copy())
flipped_vertices = _flip_polygon(poly, True, True, (10, 233))
poly.flip_ip(True, True, (10, 233))
self.assert_vertices_equal(poly.vertices, flipped_vertices)

poly = Polygon(_some_vertices.copy())
flipped_vertices = _flip_polygon(poly, True, True, (-10, -233))
poly.flip_ip(True, True, (-10, -233))
self.assert_vertices_equal(poly.vertices, flipped_vertices)


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