From 9c2200a6c18c1d488b3c0424af185b643e812664 Mon Sep 17 00:00:00 2001 From: itzpr3d4t0r <103119829+itzpr3d4t0r@users.noreply.github.com> Date: Sat, 22 Jul 2023 17:14:56 +0200 Subject: [PATCH] added Polygon flip / flip_ip --- docs/geometry.rst | 4 ++ docs/polygon.rst | 36 ++++++++++- geometry.pyi | 6 ++ src_c/polygon.c | 75 +++++++++++++++++++++++ test/test_polygon.py | 142 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 262 insertions(+), 1 deletion(-) diff --git a/docs/geometry.rst b/docs/geometry.rst index 528f7bcb..559ade94 100644 --- a/docs/geometry.rst +++ b/docs/geometry.rst @@ -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 diff --git a/docs/polygon.rst b/docs/polygon.rst index bb37fd5a..f44ae69c 100644 --- a/docs/polygon.rst +++ b/docs/polygon.rst @@ -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 ## \ No newline at end of file + .. ## 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 ## \ No newline at end of file diff --git a/geometry.pyi b/geometry.pyi index 02eca2d7..c81b00d2 100644 --- a/geometry.pyi +++ b/geometry.pyi @@ -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 diff --git a/src_c/polygon.c b/src_c/polygon.c index 1308ff68..5dff50d3 100644 --- a/src_c/polygon.c +++ b/src_c/polygon.c @@ -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}, @@ -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 * diff --git a/test/test_polygon.py b/test/test_polygon.py index 398fea12..aa23ed89 100644 --- a/test/test_polygon.py +++ b/test/test_polygon.py @@ -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 @@ -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()