From 5bdcb95b1ffff472db30ab9783541be8b0c81554 Mon Sep 17 00:00:00 2001 From: Marko Zivic Date: Sat, 28 Sep 2024 13:35:09 +0200 Subject: [PATCH 1/8] aapolygon "filled" option docs examples --- buildconfig/stubs/pygame/draw.pyi | 6 + .../ref/code_examples/draw_module_example.png | Bin 8645 -> 13445 bytes .../ref/code_examples/draw_module_example.py | 9 +- docs/reST/ref/draw.rst | 40 +++ src_c/doc/draw_doc.h | 1 + src_c/draw.c | 235 +++++++++++++++++- 6 files changed, 280 insertions(+), 11 deletions(-) diff --git a/buildconfig/stubs/pygame/draw.pyi b/buildconfig/stubs/pygame/draw.pyi index 57bae66712..bf586a12ac 100644 --- a/buildconfig/stubs/pygame/draw.pyi +++ b/buildconfig/stubs/pygame/draw.pyi @@ -21,6 +21,12 @@ def polygon( points: SequenceLike[Coordinate], width: int = 0, ) -> Rect: ... +def aapolygon( + surface: Surface, + color: ColorLike, + points: SequenceLike[Coordinate], + 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 f6fef800dd03b5c75df9871361e6bf2815049f53..bf0e4ffeb44a4e00ea7353433d0a78756d12b556 100644 GIT binary patch literal 13445 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 literal 8645 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 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 f9300ee153..fe3a1366d1 100644 --- a/docs/reST/ref/draw.rst +++ b/docs/reST/ref/draw.rst @@ -147,6 +147,46 @@ object around the draw calls (see :func:`pygame.Surface.lock` and .. ## 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 + + .. note:: + For an aapolygon, use :func:`aalines()` with ``closed=True``. + + .. 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 06a8149ef0..604489c6a0 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -82,6 +82,9 @@ draw_ellipse_thickness(SDL_Surface *surf, int x0, int y0, int width, static void draw_fillpoly(SDL_Surface *surf, int *vx, int *vy, Py_ssize_t n, Uint32 color, int *drawn_area); +static void +draw_fillpoly_for_aapolygon(SDL_Surface *surf, int *vx, int *vy, Py_ssize_t n, + Uint32 color, int *drawn_area); static int draw_filltri(SDL_Surface *surf, int *xlist, int *ylist, Uint32 color, int *drawn_area); @@ -981,6 +984,127 @@ 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, l, t; + 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. */ + } + + 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) + + if (!PySequence_Check(points)) { + return RAISE(PyExc_TypeError, + "points argument must be a sequence of number pairs"); + } + + length = PySequence_Length(points); + + if (length < 3) { + return RAISE(PyExc_ValueError, + "points argument must contain more than 2 points"); + } + + 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); + if (loop == 0) { + l = x; + t = 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_for_aapolygon(surf, xlist, ylist, length, color, + drawn_area); + } + else { + draw_filltri(surf, xlist, ylist, color, drawn_area); + } + 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) { @@ -2873,24 +2997,20 @@ static void draw_fillpoly(SDL_Surface *surf, int *point_x, int *point_y, Py_ssize_t num_points, Uint32 color, int *drawn_area) { - /* 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++) { @@ -2898,9 +3018,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]; @@ -2984,6 +3103,100 @@ draw_fillpoly(SDL_Surface *surf, int *point_x, int *point_y, PyMem_Free(x_intersect); } +/* This is same algorythm as draw_fillpoly but modified for filled + * draw.aapolygon so borders of fillpoly are not covering outer + * antialiased pixels from draw.aalines + */ +static void +draw_fillpoly_for_aapolygon(SDL_Surface *surf, int *point_x, int *point_y, + Py_ssize_t num_points, Uint32 color, + int *drawn_area) +{ + Py_ssize_t i, i_previous; + int y, miny, maxy; + int x1, y1; + int x2, y2; + float intersect; + int *x_intersect = PyMem_New(int, num_points); + if (x_intersect == NULL) { + PyErr_NoMemory(); + return; + } + + /* Determine Y bounds */ + miny = point_y[0]; + maxy = point_y[0]; + for (i = 1; (i < num_points); i++) { + miny = MIN(miny, point_y[i]); + maxy = MAX(maxy, point_y[i]); + } + + /* Special case: polygon only 1 pixel high. */ + if (miny == maxy) { + /* Determine X bounds */ + int minx = point_x[0]; + int maxx = point_x[0]; + for (i = 1; (i < num_points); i++) { + minx = MIN(minx, point_x[i]); + maxx = MAX(maxx, point_x[i]); + } + drawhorzlineclipbounding(surf, color, minx, miny, maxx, drawn_area); + PyMem_Free(x_intersect); + return; + } + + for (y = miny; (y <= maxy); y++) { + int n_intersections = 0; + for (i = 0; (i < num_points); i++) { + i_previous = ((i) ? (i - 1) : (num_points - 1)); + + y1 = point_y[i_previous]; + y2 = point_y[i]; + if (y1 < y2) { + x1 = point_x[i_previous]; + x2 = point_x[i]; + } + else if (y1 > y2) { + y2 = point_y[i_previous]; + y1 = point_y[i]; + x2 = point_x[i_previous]; + x1 = point_x[i]; + } + else { // y1 == y2 : has to be handled as special case (below) + continue; + } + if (((y >= y1) && (y < y2)) || ((y == maxy) && (y2 == maxy))) { + intersect = (y - y1) * (x2 - x1) / (float)(y2 - y1); + if (n_intersections % 2 == 0) { + // here, 1 is added so lower half is moved right + intersect = (float)floor(intersect) + 1; + } + else + intersect = (float)ceil(intersect); + x_intersect[n_intersections++] = (int)intersect + x1; + } + } + qsort(x_intersect, n_intersections, sizeof(int), compare_int); + for (i = 0; (i < n_intersections); i += 2) { + // here, 1 is subtracted, so right X coordinate is moved left + drawhorzlineclipbounding(surf, color, x_intersect[i], y, + x_intersect[i + 1] - 1, drawn_area); + } + } + + // special case + for (i = 0; (i < num_points); i++) { + i_previous = ((i) ? (i - 1) : (num_points - 1)); + y = point_y[i]; + + if ((miny < y) && (point_y[i_previous] == y) && (y < maxy)) { + drawhorzlineclipbounding(surf, color, point_x[i], y, + point_x[i_previous], drawn_area); + } + } + PyMem_Free(x_intersect); +} + static void draw_rect(SDL_Surface *surf, int x1, int y1, int x2, int y2, int width, Uint32 color) @@ -3137,6 +3350,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}}; From 3eddbf1be1194cf42dfe7648258e4ac97b415fb4 Mon Sep 17 00:00:00 2001 From: Marko Zivic Date: Sat, 28 Sep 2024 15:22:33 +0200 Subject: [PATCH 2/8] Tests --- src_c/draw.c | 25 +- test/draw_test.py | 646 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 659 insertions(+), 12 deletions(-) diff --git a/src_c/draw.c b/src_c/draw.c index 604489c6a0..280711fc68 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -1005,6 +1005,19 @@ aapolygon(PyObject *self, PyObject *arg, PyObject *kwargs) 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); @@ -1029,18 +1042,6 @@ aapolygon(PyObject *self, PyObject *arg, PyObject *kwargs) CHECK_LOAD_COLOR(colorobj) - if (!PySequence_Check(points)) { - return RAISE(PyExc_TypeError, - "points argument must be a sequence of number pairs"); - } - - length = PySequence_Length(points); - - if (length < 3) { - return RAISE(PyExc_ValueError, - "points argument must contain more than 2 points"); - } - xlist = PyMem_New(int, length); ylist = PyMem_New(int, length); diff --git a/test/draw_test.py b/test/draw_test.py index a9e493e687..f9e7b14cd8 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) @@ -4274,6 +4275,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 ############################################################## @@ -7146,6 +7788,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): @@ -7176,6 +7819,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 From ce9b788f1dac8cd9ef0d26f6157f32f48ee84c78 Mon Sep 17 00:00:00 2001 From: Marko Zivic Date: Sat, 28 Sep 2024 15:52:52 +0200 Subject: [PATCH 3/8] Removed unneeded variables --- src_c/draw.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src_c/draw.c b/src_c/draw.c index 280711fc68..75d6341c7f 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -993,7 +993,7 @@ aapolygon(PyObject *self, PyObject *arg, PyObject *kwargs) Uint32 color; int *xlist = NULL, *ylist = NULL; int filled = 1; /* filled by default */ - int x, y, result, l, t; + 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; @@ -1059,10 +1059,6 @@ aapolygon(PyObject *self, PyObject *arg, PyObject *kwargs) for (loop = 0; loop < length; ++loop) { item = PySequence_GetItem(points, loop); result = pg_TwoIntsFromObj(item, &x, &y); - if (loop == 0) { - l = x; - t = y; - } Py_DECREF(item); if (!result) { From 9940672062a412ba8c468ba8d497075cfeabe5a4 Mon Sep 17 00:00:00 2001 From: Marko Zivic Date: Sun, 29 Sep 2024 00:30:24 +0200 Subject: [PATCH 4/8] Added antialiasing for triangle --- src_c/draw.c | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src_c/draw.c b/src_c/draw.c index 75d6341c7f..804f80d74e 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -88,6 +88,9 @@ draw_fillpoly_for_aapolygon(SDL_Surface *surf, int *vx, int *vy, Py_ssize_t n, static int draw_filltri(SDL_Surface *surf, int *xlist, int *ylist, Uint32 color, int *drawn_area); +static int +draw_filltri_for_aapolygon(SDL_Surface *surf, int *xlist, int *ylist, + Uint32 color, int *drawn_area); static void draw_rect(SDL_Surface *surf, int x1, int y1, int x2, int y2, int width, Uint32 color); @@ -1082,7 +1085,7 @@ aapolygon(PyObject *self, PyObject *arg, PyObject *kwargs) drawn_area); } else { - draw_filltri(surf, xlist, ylist, color, drawn_area); + draw_filltri_for_aapolygon(surf, xlist, ylist, color, drawn_area); } PyMem_Free(xlist); PyMem_Free(ylist); @@ -1780,6 +1783,64 @@ draw_filltri(SDL_Surface *surf, int *xlist, int *ylist, Uint32 color, return 0; } +/* This is same algorythm as draw_filltri but modified for filled + * draw.aapolygon so borders of fillpoly are not covering outer + * antialiased pixels from draw.aalines + */ +static int +draw_filltri_for_aapolygon(SDL_Surface *surf, int *xlist, int *ylist, + Uint32 color, int *draw_area) +{ + int p0x, p0y, p1x, p1y, p2x, p2y; + + p0x = xlist[0]; + p1x = xlist[1]; + p2x = xlist[2]; + p0y = ylist[0]; + p1y = ylist[1]; + p2y = ylist[2]; + + if (p1y < p0y) { + swap_coordinates(&p1x, &p1y, &p0x, &p0y); + } + + if (p2y < p1y) { + swap_coordinates(&p1x, &p1y, &p2x, &p2y); + + if (p1y < p0y) { + swap_coordinates(&p1x, &p1y, &p0x, &p0y); + } + } + + if ((p0y == p1y) && (p1y == p2y) && (p0x == p1x) && (p1x != p2x)) { + swap_coordinates(&p1x, &p1y, &p2x, &p2y); + } + + float d1 = (float)((p2x - p0x) / ((p2y - p0y) + 1e-17)); + 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) + 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, draw_area); + } + } + else { + drawhorzlineclipbounding(surf, color, x1, y, x2, draw_area); + } + } + + return 0; +} + static void draw_line_width(SDL_Surface *surf, Uint32 color, int x1, int y1, int x2, int y2, int width, int *drawn_area) From e7b41203699f4ec80a0183a5ee6d3049d0800891 Mon Sep 17 00:00:00 2001 From: Marko Zivic Date: Sun, 29 Sep 2024 14:05:58 +0200 Subject: [PATCH 5/8] Avoiding duplicate code --- src_c/draw.c | 214 ++++++++++----------------------------------------- 1 file changed, 39 insertions(+), 175 deletions(-) diff --git a/src_c/draw.c b/src_c/draw.c index 804f80d74e..8141371cdf 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -81,16 +81,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); -static void -draw_fillpoly_for_aapolygon(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); -static int -draw_filltri_for_aapolygon(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); @@ -966,10 +960,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); @@ -1081,11 +1075,10 @@ aapolygon(PyObject *self, PyObject *arg, PyObject *kwargs) } if (length != 3) { - draw_fillpoly_for_aapolygon(surf, xlist, ylist, length, color, - drawn_area); + draw_fillpoly(surf, xlist, ylist, length, color, drawn_area, 1); } else { - draw_filltri_for_aapolygon(surf, xlist, ylist, color, drawn_area); + draw_filltri(surf, xlist, ylist, color, drawn_area, 1); } PyMem_Free(xlist); PyMem_Free(ylist); @@ -1737,7 +1730,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; @@ -1768,73 +1761,36 @@ 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); - - 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); - } - - return 0; -} - -/* This is same algorythm as draw_filltri but modified for filled - * draw.aapolygon so borders of fillpoly are not covering outer - * antialiased pixels from draw.aalines - */ -static int -draw_filltri_for_aapolygon(SDL_Surface *surf, int *xlist, int *ylist, - Uint32 color, int *draw_area) -{ - int p0x, p0y, p1x, p1y, p2x, p2y; - - p0x = xlist[0]; - p1x = xlist[1]; - p2x = xlist[2]; - p0y = ylist[0]; - p1y = ylist[1]; - p2y = ylist[2]; - - if (p1y < p0y) { - swap_coordinates(&p1x, &p1y, &p0x, &p0y); - } - - if (p2y < p1y) { - swap_coordinates(&p1x, &p1y, &p2x, &p2y); - - if (p1y < p0y) { - swap_coordinates(&p1x, &p1y, &p0x, &p0y); + 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); - if ((p0y == p1y) && (p1y == p2y) && (p0x == p1x) && (p1x != p2x)) { - swap_coordinates(&p1x, &p1y, &p2x, &p2y); - } + int x2; + if (y < p1y) + x2 = p0x + (int)((y - p0y) * d2); + else + x2 = p1x + (int)((y - p1y) * d3); - float d1 = (float)((p2x - p0x) / ((p2y - p0y) + 1e-17)); - 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) + 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, draw_area); - } - } - else { - drawhorzlineclipbounding(surf, color, x1, y, x2, draw_area); + drawhorzlineclipbounding(surf, color, x1, y, x2, drawn_area); } } @@ -3053,7 +3009,7 @@ 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) { Py_ssize_t i, i_previous; // index of the point before i int y, miny, maxy; @@ -3126,7 +3082,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); @@ -3135,8 +3092,9 @@ 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); } } @@ -3161,100 +3119,6 @@ draw_fillpoly(SDL_Surface *surf, int *point_x, int *point_y, PyMem_Free(x_intersect); } -/* This is same algorythm as draw_fillpoly but modified for filled - * draw.aapolygon so borders of fillpoly are not covering outer - * antialiased pixels from draw.aalines - */ -static void -draw_fillpoly_for_aapolygon(SDL_Surface *surf, int *point_x, int *point_y, - Py_ssize_t num_points, Uint32 color, - int *drawn_area) -{ - Py_ssize_t i, i_previous; - int y, miny, maxy; - int x1, y1; - int x2, y2; - float intersect; - int *x_intersect = PyMem_New(int, num_points); - if (x_intersect == NULL) { - PyErr_NoMemory(); - return; - } - - /* Determine Y bounds */ - miny = point_y[0]; - maxy = point_y[0]; - for (i = 1; (i < num_points); i++) { - miny = MIN(miny, point_y[i]); - maxy = MAX(maxy, point_y[i]); - } - - /* Special case: polygon only 1 pixel high. */ - if (miny == maxy) { - /* Determine X bounds */ - int minx = point_x[0]; - int maxx = point_x[0]; - for (i = 1; (i < num_points); i++) { - minx = MIN(minx, point_x[i]); - maxx = MAX(maxx, point_x[i]); - } - drawhorzlineclipbounding(surf, color, minx, miny, maxx, drawn_area); - PyMem_Free(x_intersect); - return; - } - - for (y = miny; (y <= maxy); y++) { - int n_intersections = 0; - for (i = 0; (i < num_points); i++) { - i_previous = ((i) ? (i - 1) : (num_points - 1)); - - y1 = point_y[i_previous]; - y2 = point_y[i]; - if (y1 < y2) { - x1 = point_x[i_previous]; - x2 = point_x[i]; - } - else if (y1 > y2) { - y2 = point_y[i_previous]; - y1 = point_y[i]; - x2 = point_x[i_previous]; - x1 = point_x[i]; - } - else { // y1 == y2 : has to be handled as special case (below) - continue; - } - if (((y >= y1) && (y < y2)) || ((y == maxy) && (y2 == maxy))) { - intersect = (y - y1) * (x2 - x1) / (float)(y2 - y1); - if (n_intersections % 2 == 0) { - // here, 1 is added so lower half is moved right - intersect = (float)floor(intersect) + 1; - } - else - intersect = (float)ceil(intersect); - x_intersect[n_intersections++] = (int)intersect + x1; - } - } - qsort(x_intersect, n_intersections, sizeof(int), compare_int); - for (i = 0; (i < n_intersections); i += 2) { - // here, 1 is subtracted, so right X coordinate is moved left - drawhorzlineclipbounding(surf, color, x_intersect[i], y, - x_intersect[i + 1] - 1, drawn_area); - } - } - - // special case - for (i = 0; (i < num_points); i++) { - i_previous = ((i) ? (i - 1) : (num_points - 1)); - y = point_y[i]; - - if ((miny < y) && (point_y[i_previous] == y) && (y < maxy)) { - drawhorzlineclipbounding(surf, color, point_x[i], y, - point_x[i_previous], drawn_area); - } - } - PyMem_Free(x_intersect); -} - static void draw_rect(SDL_Surface *surf, int x1, int y1, int x2, int y2, int width, Uint32 color) @@ -3316,7 +3180,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, From 82d776b2856f9d0442ec63b152e8ba1ba65f41c2 Mon Sep 17 00:00:00 2001 From: Marko Zivic Date: Sun, 29 Sep 2024 14:08:35 +0200 Subject: [PATCH 6/8] Formatted code --- src_c/draw.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src_c/draw.c b/src_c/draw.c index 8141371cdf..b14486144f 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -3009,7 +3009,8 @@ 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, int aapolygon_fix) + Py_ssize_t num_points, Uint32 color, int *drawn_area, + int aapolygon_fix) { Py_ssize_t i, i_previous; // index of the point before i int y, miny, maxy; @@ -3092,9 +3093,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 + // for aapolygon, 1 is subtracted, so right x coordinate is moved + // left drawhorzlineclipbounding(surf, color, x_intersect[i], y, - x_intersect[i + 1] - aapolygon_fix, drawn_area); + x_intersect[i + 1] - aapolygon_fix, + drawn_area); } } From 20862bc99dff16fab4f9d904a868b1c6c47357a6 Mon Sep 17 00:00:00 2001 From: Marko Zivic Date: Wed, 2 Oct 2024 03:09:18 +0200 Subject: [PATCH 7/8] Cleaned docs --- docs/reST/ref/draw.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/reST/ref/draw.rst b/docs/reST/ref/draw.rst index fe3a1366d1..d6c3763bba 100644 --- a/docs/reST/ref/draw.rst +++ b/docs/reST/ref/draw.rst @@ -140,9 +140,6 @@ 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 ## @@ -180,9 +177,6 @@ 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``. - .. versionadded:: 2.6.0 .. ## pygame.draw.aapolygon ## From c66ab66524c70f72057e6df5e0e28b323720cdb0 Mon Sep 17 00:00:00 2001 From: Marko Zivic <92328096+mzivic7@users.noreply.github.com> Date: Sat, 5 Oct 2024 16:30:01 +0200 Subject: [PATCH 8/8] Update draw.pyi with Point --- buildconfig/stubs/pygame/draw.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildconfig/stubs/pygame/draw.pyi b/buildconfig/stubs/pygame/draw.pyi index 6ab6639a44..ca7c694b38 100644 --- a/buildconfig/stubs/pygame/draw.pyi +++ b/buildconfig/stubs/pygame/draw.pyi @@ -24,7 +24,7 @@ def polygon( def aapolygon( surface: Surface, color: ColorLike, - points: SequenceLike[Coordinate], + points: SequenceLike[Point], filled: bool = True, ) -> Rect: ... def circle(