diff --git a/buildconfig/stubs/pygame/draw.pyi b/buildconfig/stubs/pygame/draw.pyi index b6d057f879..ca7c694b38 100644 --- a/buildconfig/stubs/pygame/draw.pyi +++ b/buildconfig/stubs/pygame/draw.pyi @@ -21,6 +21,12 @@ def polygon( points: SequenceLike[Point], width: int = 0, ) -> Rect: ... +def aapolygon( + surface: Surface, + color: ColorLike, + points: SequenceLike[Point], + filled: bool = True, +) -> Rect: ... def circle( surface: Surface, color: ColorLike, diff --git a/docs/reST/ref/code_examples/draw_module_example.png b/docs/reST/ref/code_examples/draw_module_example.png index f6fef800dd..bf0e4ffeb4 100644 Binary files a/docs/reST/ref/code_examples/draw_module_example.png and b/docs/reST/ref/code_examples/draw_module_example.png differ diff --git a/docs/reST/ref/code_examples/draw_module_example.py b/docs/reST/ref/code_examples/draw_module_example.py index ce13298477..558e4d2d8f 100644 --- a/docs/reST/ref/code_examples/draw_module_example.py +++ b/docs/reST/ref/code_examples/draw_module_example.py @@ -65,9 +65,16 @@ # Draw an solid ellipse, using a rectangle as the outside boundaries pygame.draw.ellipse(screen, "red", [300, 10, 50, 20]) - # This draws a triangle using the polygon command + # Draw a triangle using the polygon command pygame.draw.polygon(screen, "black", [[100, 100], [0, 200], [200, 200]], 5) + # Draw an antialiased polygon + pygame.draw.aapolygon( + screen, + "black", + [[100, 40], [150, 75], [110, 60], [70, 70]] + ) + # Draw an arc as part of an ellipse. # Use radians to determine what angle to draw. pygame.draw.arc(screen, "black", [210, 75, 150, 125], 0, pi / 2, 2) diff --git a/docs/reST/ref/draw.rst b/docs/reST/ref/draw.rst index acf95873cd..7b2149b42f 100644 --- a/docs/reST/ref/draw.rst +++ b/docs/reST/ref/draw.rst @@ -140,13 +140,47 @@ object around the draw calls (see :func:`pygame.Surface.lock` and :raises TypeError: if ``points`` is not a sequence or ``points`` does not contain number pairs - .. note:: - For an aapolygon, use :func:`aalines()` with ``closed=True``. - .. versionchangedold:: 2.0.0 Added support for keyword arguments. .. ## pygame.draw.polygon ## +.. function:: aapolygon + + | :sl:`draw an antialiased polygon` + | :sg:`aapolygon(surface, color, points) -> Rect` + | :sg:`aapolygon(surface, color, points, filled=True) -> Rect` + + Draws an antialiased polygon on the given surface. + + :param Surface surface: surface to draw on + :param color: color to draw with, the alpha value is optional if using a + tuple ``(RGB[A])`` + :type color: Color or string (for :doc:`color_list`) or int or tuple(int, int, int, [int]) + :param points: a sequence of 3 or more (x, y) coordinates that make up the + vertices of the polygon, each *coordinate* in the sequence must be a + tuple/list/:class:`pygame.math.Vector2` of 2 ints/floats, + e.g. ``[(x1, y1), (x2, y2), (x3, y3)]`` + :type points: tuple(coordinate) or list(coordinate) + :param int filled: (optional) used to indicate that the polygon is to be filled + + | if filled == True, (default) fill the polygon + | if filled == False, don't fill the polygon + | + + :returns: a rect bounding the changed pixels, if nothing is drawn the + bounding rect's position will be the position of the first point in the + ``points`` parameter (float values will be truncated) and its width and + height will be 0 + :rtype: Rect + + :raises ValueError: if ``len(points) < 3`` (must have at least 3 points) + :raises TypeError: if ``points`` is not a sequence or ``points`` does not + contain number pairs + + .. versionadded:: 2.6.0 + + .. ## pygame.draw.aapolygon ## + .. function:: circle | :sl:`draw a circle` diff --git a/src_c/doc/draw_doc.h b/src_c/doc/draw_doc.h index b47ac37250..65b729f03f 100644 --- a/src_c/doc/draw_doc.h +++ b/src_c/doc/draw_doc.h @@ -2,6 +2,7 @@ #define DOC_DRAW "pygame module for drawing shapes" #define DOC_DRAW_RECT "rect(surface, color, rect) -> Rect\nrect(surface, color, rect, width=0, border_radius=0, border_top_left_radius=-1, border_top_right_radius=-1, border_bottom_left_radius=-1, border_bottom_right_radius=-1) -> Rect\ndraw a rectangle" #define DOC_DRAW_POLYGON "polygon(surface, color, points) -> Rect\npolygon(surface, color, points, width=0) -> Rect\ndraw a polygon" +#define DOC_DRAW_AAPOLYGON "aapolygon(surface, color, points) -> Rect\naapolygon(surface, color, points, filled=True) -> Rect\ndraw an antialiased polygon" #define DOC_DRAW_CIRCLE "circle(surface, color, center, radius) -> Rect\ncircle(surface, color, center, radius, width=0, draw_top_right=None, draw_top_left=None, draw_bottom_left=None, draw_bottom_right=None) -> Rect\ndraw a circle" #define DOC_DRAW_AACIRCLE "aacircle(surface, color, center, radius) -> Rect\naacircle(surface, color, center, radius, width=0, draw_top_right=None, draw_top_left=None, draw_bottom_left=None, draw_bottom_right=None) -> Rect\ndraw an antialiased circle" #define DOC_DRAW_ELLIPSE "ellipse(surface, color, rect) -> Rect\nellipse(surface, color, rect, width=0) -> Rect\ndraw an ellipse" diff --git a/src_c/draw.c b/src_c/draw.c index 115a6bfd26..a9dcfcfe14 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -83,10 +83,10 @@ draw_ellipse_thickness(SDL_Surface *surf, int x0, int y0, int width, int *drawn_area); static void draw_fillpoly(SDL_Surface *surf, int *vx, int *vy, Py_ssize_t n, Uint32 color, - int *drawn_area); + int *drawn_area, int aapolygon_fix); static int draw_filltri(SDL_Surface *surf, int *xlist, int *ylist, Uint32 color, - int *drawn_area); + int *drawn_area, int aapolygon_fix); static void draw_rect(SDL_Surface *surf, int x1, int y1, int x2, int y2, int width, Uint32 color); @@ -1031,10 +1031,10 @@ polygon(PyObject *self, PyObject *arg, PyObject *kwargs) } if (length != 3) { - draw_fillpoly(surf, xlist, ylist, length, color, drawn_area); + draw_fillpoly(surf, xlist, ylist, length, color, drawn_area, 0); } else { - draw_filltri(surf, xlist, ylist, color, drawn_area); + draw_filltri(surf, xlist, ylist, color, drawn_area, 0); } PyMem_Free(xlist); PyMem_Free(ylist); @@ -1052,6 +1052,123 @@ polygon(PyObject *self, PyObject *arg, PyObject *kwargs) return pgRect_New4(l, t, 0, 0); } +static PyObject * +aapolygon(PyObject *self, PyObject *arg, PyObject *kwargs) +{ + pgSurfaceObject *surfobj; + PyObject *colorobj, *points, *item = NULL; + SDL_Surface *surf = NULL; + Uint32 color; + int *xlist = NULL, *ylist = NULL; + int filled = 1; /* filled by default */ + int x, y, result; + int drawn_area[4] = {INT_MAX, INT_MAX, INT_MIN, + INT_MIN}; /* Used to store bounding box values */ + Py_ssize_t loop, length; + static char *keywords[] = {"surface", "color", "points", "filled", NULL}; + + if (!PyArg_ParseTupleAndKeywords(arg, kwargs, "O!OO|p", keywords, + &pgSurface_Type, &surfobj, &colorobj, + &points, &filled)) { + return NULL; /* Exception already set. */ + } + + length = PySequence_Length(points); + + if (!PySequence_Check(points)) { + return RAISE(PyExc_TypeError, + "points argument must be a sequence of number pairs"); + } + + // always check this because aalines accepts 2 points + if (length < 3) { + return RAISE(PyExc_ValueError, + "points argument must contain more than 2 points"); + } + + if (filled == 0) { + PyObject *ret = NULL; + PyObject *args = Py_BuildValue("(OOiO)", surfobj, colorobj, 1, points); + + if (!args) { + return NULL; /* Exception already set. */ + } + + ret = aalines(NULL, args, NULL); + Py_DECREF(args); + return ret; + } + + surf = pgSurface_AsSurface(surfobj); + SURF_INIT_CHECK(surf) + + if (PG_SURF_BytesPerPixel(surf) <= 0 || PG_SURF_BytesPerPixel(surf) > 4) { + return PyErr_Format(PyExc_ValueError, + "unsupported surface bit depth (%d) for drawing", + PG_SURF_BytesPerPixel(surf)); + } + + CHECK_LOAD_COLOR(colorobj) + + xlist = PyMem_New(int, length); + ylist = PyMem_New(int, length); + + if (NULL == xlist || NULL == ylist) { + if (xlist) { + PyMem_Free(xlist); + } + if (ylist) { + PyMem_Free(ylist); + } + return RAISE(PyExc_MemoryError, + "cannot allocate memory to draw polygon"); + } + + for (loop = 0; loop < length; ++loop) { + item = PySequence_GetItem(points, loop); + result = pg_TwoIntsFromObj(item, &x, &y); + Py_DECREF(item); + + if (!result) { + PyMem_Free(xlist); + PyMem_Free(ylist); + return RAISE(PyExc_TypeError, "points must be number pairs"); + } + + xlist[loop] = x; + ylist[loop] = y; + } + + if (!pgSurface_Lock(surfobj)) { + PyMem_Free(xlist); + PyMem_Free(ylist); + return RAISE(PyExc_RuntimeError, "error locking surface"); + } + + if (length != 3) { + draw_fillpoly(surf, xlist, ylist, length, color, drawn_area, 1); + } + else { + draw_filltri(surf, xlist, ylist, color, drawn_area, 1); + } + PyMem_Free(xlist); + PyMem_Free(ylist); + + // aalines for antialiasing + PyObject *ret = NULL; + PyObject *args = Py_BuildValue("(OOiO)", surfobj, colorobj, 1, points); + if (!args) { + return NULL; /* Exception already set. */ + } + ret = aalines(NULL, args, NULL); + Py_DECREF(args); + + if (!pgSurface_Unlock(surfobj)) { + return RAISE(PyExc_RuntimeError, "error unlocking surface"); + } + return ret; // already calculated return rect in aalines +} + static PyObject * rect(PyObject *self, PyObject *args, PyObject *kwargs) { @@ -1698,7 +1815,7 @@ swap_coordinates(int *x1, int *y1, int *x2, int *y2) static int draw_filltri(SDL_Surface *surf, int *xlist, int *ylist, Uint32 color, - int *draw_area) + int *drawn_area, int aapolygon_fix) { int p0x, p0y, p1x, p1y, p2x, p2y; @@ -1729,16 +1846,37 @@ draw_filltri(SDL_Surface *surf, int *xlist, int *ylist, Uint32 color, float d2 = (float)((p1x - p0x) / ((p1y - p0y) + 1e-17)); float d3 = (float)((p2x - p1x) / ((p2y - p1y) + 1e-17)); int y; - for (y = p0y; y <= p2y; y++) { - int x1 = p0x + (int)((y - p0y) * d1); + if (aapolygon_fix) { + for (y = p0y; y <= p2y; y++) { + int x1 = p0x + (int)((y - p0y) * d1) + 1; + + int x2; + if (y < p1y) + x2 = p0x + (int)((y - p0y) * d2); + else + x2 = p1x + (int)((y - p1y) * d3) - 1; + if (x1 > x2) { + if (x1 - x2 != 1) { + set_and_check_rect(surf, x1 - 1, y, color, drawn_area); + } + } + else { + drawhorzlineclipbounding(surf, color, x1, y, x2, drawn_area); + } + } + } + else { + for (y = p0y; y <= p2y; y++) { + int x1 = p0x + (int)((y - p0y) * d1); - int x2; - if (y < p1y) - x2 = p0x + (int)((y - p0y) * d2); - else - x2 = p1x + (int)((y - p1y) * d3); + int x2; + if (y < p1y) + x2 = p0x + (int)((y - p0y) * d2); + else + x2 = p1x + (int)((y - p1y) * d3); - drawhorzlineclipbounding(surf, color, x1, y, x2, draw_area); + drawhorzlineclipbounding(surf, color, x1, y, x2, drawn_area); + } } return 0; @@ -2956,26 +3094,23 @@ draw_ellipse_thickness(SDL_Surface *surf, int x0, int y0, int width, static void draw_fillpoly(SDL_Surface *surf, int *point_x, int *point_y, - Py_ssize_t num_points, Uint32 color, int *drawn_area) + Py_ssize_t num_points, Uint32 color, int *drawn_area, + int aapolygon_fix) { - /* point_x : x coordinates of the points - * point-y : the y coordinates of the points - * num_points : the number of points - */ - Py_ssize_t i, i_previous; // i_previous is the index of the point before i + Py_ssize_t i, i_previous; // index of the point before i int y, miny, maxy; int x1, y1; int x2, y2; float intersect; - /* x_intersect are the x-coordinates of intersections of the polygon - * with some horizontal line */ + /* x-coordinate of intersections of the polygon with some + horizontal line */ int *x_intersect = PyMem_New(int, num_points); if (x_intersect == NULL) { PyErr_NoMemory(); return; } - /* Determine Y maxima */ + /* Determine Y bounds */ miny = point_y[0]; maxy = point_y[0]; for (i = 1; (i < num_points); i++) { @@ -2983,9 +3118,8 @@ draw_fillpoly(SDL_Surface *surf, int *point_x, int *point_y, maxy = MAX(maxy, point_y[i]); } + /* Special case: polygon only 1 pixel high. */ if (miny == maxy) { - /* Special case: polygon only 1 pixel high. */ - /* Determine X bounds */ int minx = point_x[0]; int maxx = point_x[0]; @@ -3034,7 +3168,8 @@ draw_fillpoly(SDL_Surface *surf, int *point_x, int *point_y, // end), or when we are on the lowest line (maxy) intersect = (y - y1) * (x2 - x1) / (float)(y2 - y1); if (n_intersections % 2 == 0) { - intersect = (float)floor(intersect); + // for aapolygon, 1 is added so lower half is moved right + intersect = (float)floor(intersect) + aapolygon_fix; } else intersect = (float)ceil(intersect); @@ -3043,8 +3178,11 @@ draw_fillpoly(SDL_Surface *surf, int *point_x, int *point_y, } qsort(x_intersect, n_intersections, sizeof(int), compare_int); for (i = 0; (i < n_intersections); i += 2) { + // for aapolygon, 1 is subtracted, so right x coordinate is moved + // left drawhorzlineclipbounding(surf, color, x_intersect[i], y, - x_intersect[i + 1], drawn_area); + x_intersect[i + 1] - aapolygon_fix, + drawn_area); } } @@ -3130,7 +3268,7 @@ draw_round_rect(SDL_Surface *surf, int x1, int y1, int x2, int y2, int radius, pts[13] = y2; pts[14] = y2; pts[15] = y2 - bottom_left; - draw_fillpoly(surf, pts, pts + 8, 8, color, drawn_area); + draw_fillpoly(surf, pts, pts + 8, 8, color, drawn_area, 0); draw_circle_quadrant(surf, x2 - top_right + 1, y1 + top_right, top_right, 0, color, 1, 0, 0, 0, drawn_area); draw_circle_quadrant(surf, x1 + top_left, y1 + top_left, top_left, 0, @@ -3222,6 +3360,8 @@ static PyMethodDef _draw_methods[] = { DOC_DRAW_AACIRCLE}, {"polygon", (PyCFunction)polygon, METH_VARARGS | METH_KEYWORDS, DOC_DRAW_POLYGON}, + {"aapolygon", (PyCFunction)aapolygon, METH_VARARGS | METH_KEYWORDS, + DOC_DRAW_AAPOLYGON}, {"rect", (PyCFunction)rect, METH_VARARGS | METH_KEYWORDS, DOC_DRAW_RECT}, {NULL, NULL, 0, NULL}}; diff --git a/test/draw_test.py b/test/draw_test.py index eb61d29164..8c27835eb4 100644 --- a/test/draw_test.py +++ b/test/draw_test.py @@ -166,6 +166,7 @@ class DrawTestCase(unittest.TestCase): draw_rect = staticmethod(draw.rect) draw_polygon = staticmethod(draw.polygon) + draw_aapolygon = staticmethod(draw.aapolygon) draw_circle = staticmethod(draw.circle) draw_aacircle = staticmethod(draw.aacircle) draw_ellipse = staticmethod(draw.ellipse) @@ -4328,6 +4329,647 @@ class DrawPolygonTest(DrawPolygonMixin, DrawTestCase): """ +### AAPolygon Testing ########################################################### + +SQUARE = ([0, 0], [3, 0], [3, 3], [0, 3]) +DIAMOND = [(1, 3), (3, 5), (5, 3), (3, 1)] +RHOMBUS = [(1, 3), (4, 5), (7, 3), (4, 1)] +CROSS = ( + [2, 0], + [4, 0], + [4, 2], + [6, 2], + [6, 4], + [4, 4], + [4, 6], + [2, 6], + [2, 4], + [0, 4], + [0, 2], + [2, 2], +) + + +class DrawAAPolygonMixin: + """Mixin tests for drawing aapolygons. + + This class contains all the general aapolygon drawing tests. + """ + + def setUp(self): + self.surface = pygame.Surface((20, 20)) + + def test_aapolygon__args(self): + """Ensures draw aapolygon accepts the correct args.""" + bounds_rect = self.draw_aapolygon( + pygame.Surface((3, 3)), (0, 10, 0, 50), ((0, 0), (1, 1), (2, 2)), False + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aapolygon__args_without_filled(self): + """Ensures draw aapolygon accepts the args without filled arg.""" + bounds_rect = self.draw_aapolygon( + pygame.Surface((2, 2)), (0, 0, 0, 50), ((0, 0), (1, 1), (2, 2)) + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aapolygon__kwargs(self): + """Ensures draw aapolygon accepts the correct kwargs + with and without a filled arg. + """ + surface = pygame.Surface((4, 4)) + color = pygame.Color("yellow") + points = ((0, 0), (1, 1), (2, 2)) + kwargs_list = [ + {"surface": surface, "color": color, "points": points, "filled": False}, + {"surface": surface, "color": color, "points": points}, + ] + + for kwargs in kwargs_list: + bounds_rect = self.draw_aapolygon(**kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aapolygon__kwargs_order_independent(self): + """Ensures draw aapolygon's kwargs are not order dependent.""" + bounds_rect = self.draw_aapolygon( + color=(10, 20, 30), + surface=pygame.Surface((3, 2)), + filled=False, + points=((0, 1), (1, 2), (2, 3)), + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aapolygon__args_missing(self): + """Ensures draw aapolygon detects any missing required args.""" + surface = pygame.Surface((1, 1)) + color = pygame.Color("blue") + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aapolygon(surface, color) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aapolygon(surface) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aapolygon() + + def test_aapolygon__kwargs_missing(self): + """Ensures draw aapolygon detects any missing required kwargs.""" + kwargs = { + "surface": pygame.Surface((1, 2)), + "color": pygame.Color("red"), + "points": ((2, 1), (2, 2), (2, 3)), + "filled": False, + } + + for name in ("points", "color", "surface"): + invalid_kwargs = dict(kwargs) + invalid_kwargs.pop(name) # Pop from a copy. + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aapolygon(**invalid_kwargs) + + def test_aapolygon__arg_invalid_types(self): + """Ensures draw aapolygon detects invalid arg types.""" + surface = pygame.Surface((2, 2)) + color = pygame.Color("blue") + points = ((0, 1), (1, 2), (1, 3)) + + with self.assertRaises(TypeError): + # Invalid filled. + bounds_rect = self.draw_aapolygon(surface, color, points, InvalidBool()) + + with self.assertRaises(TypeError): + # Invalid points. + bounds_rect = self.draw_aapolygon(surface, color, (1, 2, 3)) + + with self.assertRaises(TypeError): + # Invalid color. + bounds_rect = self.draw_aapolygon(surface, 2.3, points) + + with self.assertRaises(TypeError): + # Invalid surface. + bounds_rect = self.draw_aapolygon((1, 2, 3, 4), color, points) + + def test_aapolygon__kwarg_invalid_types(self): + """Ensures draw aapolygon detects invalid kwarg types.""" + surface = pygame.Surface((3, 3)) + color = pygame.Color("green") + points = ((0, 0), (1, 0), (2, 0)) + filled = False + kwargs_list = [ + { + "surface": pygame.Surface, # Invalid surface. + "color": color, + "points": points, + "filled": filled, + }, + { + "surface": surface, + "color": 2.3, # Invalid color. + "points": points, + "filled": filled, + }, + { + "surface": surface, + "color": color, + "points": ((1,), (1,), (1,)), # Invalid points. + "filled": filled, + }, + { + "surface": surface, + "color": color, + "points": points, + "filled": InvalidBool(), + }, + ] # Invalid filled. + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_aapolygon(**kwargs) + + def test_aapolygon__kwarg_invalid_name(self): + """Ensures draw aapolygon detects invalid kwarg names.""" + surface = pygame.Surface((2, 3)) + color = pygame.Color("cyan") + points = ((1, 1), (1, 2), (1, 3)) + kwargs_list = [ + { + "surface": surface, + "color": color, + "points": points, + "filed": False, + "invalid": 1, + }, + {"surface": surface, "color": color, "points": points, "invalid": 1}, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_aapolygon(**kwargs) + + def test_aapolygon__args_and_kwargs(self): + """Ensures draw aapolygon accepts a combination of args/kwargs""" + surface = pygame.Surface((3, 1)) + color = (255, 255, 0, 0) + points = ((0, 1), (1, 2), (2, 3)) + filled = False + kwargs = { + "surface": surface, + "color": color, + "points": points, + "filled": filled, + } + + for name in ("surface", "color", "points", "filled"): + kwargs.pop(name) + + if "surface" == name: + bounds_rect = self.draw_aapolygon(surface, **kwargs) + elif "color" == name: + bounds_rect = self.draw_aapolygon(surface, color, **kwargs) + elif "points" == name: + bounds_rect = self.draw_aapolygon(surface, color, points, **kwargs) + else: + bounds_rect = self.draw_aapolygon( + surface, color, points, filled, **kwargs + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aapolygon__valid_filled_values(self): + """Ensures draw aapolygon accepts different filled values.""" + surface_color = pygame.Color("white") + surface = pygame.Surface((3, 4)) + color = (10, 20, 30, 255) + pos = (2, 2) + kwargs = { + "surface": surface, + "color": color, + "points": ((1, 1), (3, 1), (3, 3), (1, 3)), + "filled": None, + } + + true_values = (-7, 1, 10, "2", 3.1, (4,), [5], True) + false_values = (None, "", 0, (), [], False) + + for filled in true_values + false_values: + surface.fill(surface_color) # Clear for each test. + kwargs["filled"] = filled + expected_color = color if filled else surface_color + + bounds_rect = self.draw_aapolygon(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aapolygon__valid_points_format(self): + """Ensures draw aapolygon accepts different points formats.""" + expected_color = (10, 20, 30, 255) + surface_color = pygame.Color("white") + surface = pygame.Surface((3, 4)) + kwargs = { + "surface": surface, + "color": expected_color, + "points": None, + "filled": False, + } + + # The point type can be a tuple/list/Vector2. + point_types = ( + (tuple, tuple, tuple, tuple), # all tuples + (list, list, list, list), # all lists + (Vector2, Vector2, Vector2, Vector2), # all Vector2s + (list, Vector2, tuple, Vector2), + ) # mix + + # The point values can be ints or floats. + point_values = ( + ((1, 1), (2, 1), (2, 2), (1, 2)), + ((1, 1), (2.2, 1), (2.1, 2.2), (1, 2.1)), + ) + + # Each sequence of points can be a tuple or a list. + seq_types = (tuple, list) + + for point_type in point_types: + for values in point_values: + check_pos = values[0] + points = [point_type[i](pt) for i, pt in enumerate(values)] + + for seq_type in seq_types: + surface.fill(surface_color) # Clear for each test. + kwargs["points"] = seq_type(points) + + bounds_rect = self.draw_aapolygon(**kwargs) + + self.assertEqual(surface.get_at(check_pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aapolygon__invalid_points_formats(self): + """Ensures draw aapolygon handles invalid points formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("red"), + "points": None, + "filled": False, + } + + points_fmts = ( + ((1, 1), (2, 1), (2,)), # Too few coords. + ((1, 1), (2, 1), (2, 2, 2)), # Too many coords. + ((1, 1), (2, 1), (2, "2")), # Wrong type. + ((1, 1), (2, 1), {2, 3}), # Wrong type. + ((1, 1), (2, 1), {2: 2, 3: 3}), # Wrong type. + {(1, 1), (2, 1), (2, 2), (1, 2)}, # Wrong type. + {1: 1, 2: 2, 3: 3, 4: 4}, + ) # Wrong type. + + for points in points_fmts: + kwargs["points"] = points + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aapolygon(**kwargs) + + def test_aapolygon__invalid_points_values(self): + """Ensures draw aapolygon handles invalid points values correctly.""" + kwargs = { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("red"), + "points": None, + "filled": False, + } + + points_fmts = ( + (), # Too few points. + ((1, 1),), # Too few points. + ((1, 1), (2, 1)), + ) # Too few points. + + for points in points_fmts: + for seq_type in (tuple, list): # Test as tuples and lists. + kwargs["points"] = seq_type(points) + + with self.assertRaises(ValueError): + bounds_rect = self.draw_aapolygon(**kwargs) + + def test_aapolygon__valid_color_formats(self): + """Ensures draw aapolygon accepts different color formats.""" + green_color = pygame.Color("green") + surface_color = pygame.Color("black") + surface = pygame.Surface((3, 4)) + kwargs = { + "surface": surface, + "color": None, + "points": ((1, 1), (2, 1), (2, 2), (1, 2)), + "filled": False, + } + pos = kwargs["points"][0] + greens = ( + (0, 255, 0), + (0, 255, 0, 255), + surface.map_rgb(green_color), + green_color, + ) + + for color in greens: + surface.fill(surface_color) # Clear for each test. + kwargs["color"] = color + + if isinstance(color, int): + expected_color = surface.unmap_rgb(color) + else: + expected_color = green_color + + bounds_rect = self.draw_aapolygon(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aapolygon__invalid_color_formats(self): + """Ensures draw aapolygon handles invalid color formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 3)), + "color": None, + "points": ((1, 1), (2, 1), (2, 2), (1, 2)), + "filled": False, + } + + for expected_color in (2.3, self): + kwargs["color"] = expected_color + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aapolygon(**kwargs) + + def test_draw_square(self): + self.draw_aapolygon(self.surface, RED, SQUARE) + # note : there is a discussion (#234) if draw.aapolygon should include or + # not the right or lower border; here we stick with current behavior, + # e.g. include those borders ... + for x in range(4): + for y in range(4): + self.assertEqual(self.surface.get_at((x, y)), RED) + + def test_draw_diamond(self): + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) + self.draw_aapolygon(self.surface, GREEN, DIAMOND) + # this diamond shape is equivalent to its four corners, plus inner square + for x, y in DIAMOND: + self.assertEqual(self.surface.get_at((x, y)), GREEN, msg=str((x, y))) + for x in range(2, 5): + for y in range(2, 5): + self.assertEqual(self.surface.get_at((x, y)), GREEN) + + def test_1_pixel_high_or_wide_shapes(self): + # 1. one-pixel-high, filled + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) + self.draw_aapolygon(self.surface, GREEN, [(x, 2) for x, _y in CROSS], True) + cross_size = 6 # the maximum x or y coordinate of the cross + for x in range(cross_size + 1): + self.assertEqual(self.surface.get_at((x, 1)), RED) + self.assertEqual(self.surface.get_at((x, 2)), GREEN) + self.assertEqual(self.surface.get_at((x, 3)), RED) + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) + # 2. one-pixel-high, not filled + self.draw_aapolygon(self.surface, GREEN, [(x, 5) for x, _y in CROSS], False) + for x in range(cross_size + 1): + self.assertEqual(self.surface.get_at((x, 4)), RED) + self.assertEqual(self.surface.get_at((x, 5)), GREEN) + self.assertEqual(self.surface.get_at((x, 6)), RED) + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) + # 3. one-pixel-wide, filled + self.draw_aapolygon(self.surface, GREEN, [(3, y) for _x, y in CROSS], True) + for y in range(cross_size + 1): + self.assertEqual(self.surface.get_at((2, y)), RED) + self.assertEqual(self.surface.get_at((3, y)), GREEN) + self.assertEqual(self.surface.get_at((4, y)), RED) + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) + # 4. one-pixel-wide, not filled + self.draw_aapolygon(self.surface, GREEN, [(4, y) for _x, y in CROSS], False) + for y in range(cross_size + 1): + self.assertEqual(self.surface.get_at((3, y)), RED) + self.assertEqual(self.surface.get_at((4, y)), GREEN) + self.assertEqual(self.surface.get_at((5, y)), RED) + + def test_draw_symetric_cross(self): + """non-regression on pygame-ce issue #249 : x and y where handled inconsistently. + + Also, the result is/was different whether we fill or not the polygon. + """ + # 1. case (not filled: `aapolygon` calls internally the `aalines` function) + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) + self.draw_aapolygon(self.surface, GREEN, CROSS, False) + inside = [(x, 3) for x in range(1, 6)] + [(3, y) for y in range(1, 6)] + for x in range(10): + for y in range(10): + if (x, y) in inside: + self.assertEqual(self.surface.get_at((x, y)), RED) + elif (x in range(2, 5) and y < 7) or (y in range(2, 5) and x < 7): + # we are on the border of the cross: + self.assertEqual(self.surface.get_at((x, y)), GREEN) + else: + # we are outside + self.assertEqual(self.surface.get_at((x, y)), RED) + + # 2. case (filled; this is the example from #234) + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 1) + self.draw_aapolygon(self.surface, GREEN, CROSS, True) + inside = [(x, 3) for x in range(1, 6)] + [(3, y) for y in range(1, 6)] + for x in range(10): + for y in range(10): + if (x in range(2, 5) and y < 7) or (y in range(2, 5) and x < 7): + # we are on the border of the cross: + self.assertEqual( + self.surface.get_at((x, y)), GREEN, msg=str((x, y)) + ) + else: + # we are outside + self.assertEqual(self.surface.get_at((x, y)), RED) + + def test_illumine_shape(self): + """non-regression on pygame-ce issue #328""" + rect = pygame.Rect((0, 0, 20, 20)) + path_data = [ + (0, 0), + (rect.width - 1, 0), # upper border + (rect.width - 5, 5 - 1), + (5 - 1, 5 - 1), # upper inner + (5 - 1, rect.height - 5), + (0, rect.height - 1), + ] # lower diagonal + # The shape looks like this (the numbers are the indices of path_data) + + # 0**********************1 <-- upper border + # *********************** + # ********************** + # ********************* + # ****3**************2 <-- upper inner border + # ***** + # ***** (more lines here) + # ***** + # ****4 + # **** + # *** + # ** + # 5 + # + + # the current bug is that the "upper inner" line is not drawn, but only + # if 4 or some lower corner exists + pygame.draw.rect(self.surface, RED, (0, 0, 20, 20), 0) + + # 1. First without the corners 4 & 5 + self.draw_aapolygon(self.surface, GREEN, path_data[:4]) + for x in range(20): + self.assertEqual(self.surface.get_at((x, 0)), GREEN) # upper border + for x in range(4, rect.width - 5 + 1): + self.assertEqual(self.surface.get_at((x, 4)), GREEN) # upper inner + + # 2. with the corners 4 & 5 + pygame.draw.rect(self.surface, RED, (0, 0, 20, 20), 0) + self.draw_aapolygon(self.surface, GREEN, path_data) + for x in range(4, rect.width - 5 + 1): + self.assertEqual(self.surface.get_at((x, 4)), GREEN) # upper inner + + def test_invalid_points(self): + self.assertRaises( + TypeError, + lambda: self.draw_aapolygon( + self.surface, RED, ((0, 0), (0, 20), (20, 20), 20) + ), + ) + + def test_aapolygon__bounding_rect(self): + """Ensures draw aapolygon returns the correct bounding rect. + + Tests polygons on and off the surface, filled and not filled. + """ + polygon_color = pygame.Color("red") + surf_color = pygame.Color("black") + min_width = min_height = 5 + max_width = max_height = 7 + sizes = ((min_width, min_height), (max_width, max_height)) + surface = pygame.Surface((20, 20), 0, 32) + surf_rect = surface.get_rect() + # Make a rect that is bigger than the surface to help test drawing + # polygons off and partially off the surface. + big_rect = surf_rect.inflate(min_width * 2 + 1, min_height * 2 + 1) + + for pos in rect_corners_mids_and_center( + surf_rect + ) + rect_corners_mids_and_center(big_rect): + # A rect (pos_rect) is used to help create and position the + # polygon. Each of this rect's position attributes will be set to + # the pos value. + for attr in RECT_POSITION_ATTRIBUTES: + # Test using different rect sizes and filled. + for width, height in sizes: + pos_rect = pygame.Rect((0, 0), (width, height)) + setattr(pos_rect, attr, pos) + # Points form a triangle with no fully + # horizontal/vertical lines. + vertices = ( + pos_rect.midleft, + pos_rect.midtop, + pos_rect.bottomright, + ) + + for filled in (True, False): + surface.fill(surf_color) # Clear for each test. + + bounding_rect = self.draw_aapolygon( + surface, polygon_color, vertices, filled + ) + + # Calculating the expected_rect after the polygon + # is drawn (it uses what is actually drawn). + expected_rect = create_bounding_rect( + surface, surf_color, vertices[0] + ) + + self.assertEqual( + bounding_rect, + expected_rect, + f"filled={filled}", + ) + + def test_aapolygon__surface_clip(self): + """Ensures draw aapolygon respects a surface's clip area. + + Tests drawing the polygon filled and unfilled. + """ + surfw = surfh = 30 + polygon_color = pygame.Color("red") + surface_color = pygame.Color("green") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + clip_rect = pygame.Rect((0, 0), (8, 10)) + clip_rect.center = surface.get_rect().center + pos_rect = clip_rect.copy() # Manages the polygon's pos. + + for filled in (True, False): # Filled and unfilled. + # Test centering the polygon along the clip rect's edge. + for center in rect_corners_mids_and_center(clip_rect): + # Get the expected points by drawing the polygon without the + # clip area set. + pos_rect.center = center + vertices = ( + pos_rect.topleft, + pos_rect.topright, + pos_rect.bottomright, + pos_rect.bottomleft, + ) + surface.set_clip(None) + surface.fill(surface_color) + self.draw_aapolygon(surface, polygon_color, vertices, filled) + expected_pts = get_color_points(surface, polygon_color, clip_rect) + + # Clear the surface and set the clip area. Redraw the polygon + # and check that only the clip area is modified. + surface.fill(surface_color) + surface.set_clip(clip_rect) + + self.draw_aapolygon(surface, polygon_color, vertices, filled) + + surface.lock() # For possible speed up. + + # Check all the surface points to ensure only the expected_pts + # are the polygon_color. + for pt in ((x, y) for x in range(surfw) for y in range(surfh)): + if pt in expected_pts: + expected_color = polygon_color + else: + expected_color = surface_color + + self.assertEqual(surface.get_at(pt), expected_color, pt) + + surface.unlock() + + def test_aapolygon_filled_shape(self): + """ + Modified test from draw.polygon to ensure antialiased pixels are + visible on all sides of polygon + """ + key_polygon_points = [(2, 2), (6, 2), (2, 4), (6, 4)] + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) + pygame.draw.aapolygon(self.surface, GREEN, RHOMBUS) + for x, y in key_polygon_points: + self.assertNotEqual(self.surface.get_at((x, y)), GREEN, msg=str((x, y))) + + +class DrawAAPolygonTest(DrawAAPolygonMixin, DrawTestCase): + """Test draw module function aapolygon. + + This class inherits the general tests from DrawAAPolygonMixin. It is also + the class to add any draw.aapolygon specific tests to. + """ + + ### Rect Testing ############################################################## @@ -7200,6 +7842,7 @@ def test_color_validation(self): draw.circle(surf, col, (7, 3), 2) draw.aacircle(surf, col, (7, 3), 2) draw.polygon(surf, col, points, 0) + draw.aapolygon(surf, col, points, 0) # 2. invalid colors for col in (1.256, object(), None): @@ -7230,6 +7873,9 @@ def test_color_validation(self): with self.assertRaises(TypeError): draw.polygon(surf, col, points, 0) + with self.assertRaises(TypeError): + draw.aapolygon(surf, col, points, 0) + def test_aafunctions_depth_segfault(self): """Ensure future commits don't break the segfault fixed by pull request https://github.com/pygame-community/pygame-ce/pull/3008