From 6e8840cffa99c10cd80a847d46a77184edaae156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CDahbani1=E2=80=9D?= Date: Sun, 17 Dec 2023 21:54:45 +0100 Subject: [PATCH 01/17] Added geometrical calculations for draw_round_polygon --- src_c/draw.c | 123 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 119 insertions(+), 4 deletions(-) diff --git a/src_c/draw.c b/src_c/draw.c index 4414dbb3b4..a1dcc2c826 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -85,6 +85,7 @@ draw_round_rect(SDL_Surface *surf, int x1, int y1, int x2, int y2, int radius, int width, Uint32 color, int top_left, int top_right, int bottom_left, int bottom_right, int *drawn_area); + // validation of a draw color #define CHECK_LOAD_COLOR(colorobj) \ if (!pg_MappedColorFromObj((colorobj), surf->format, &color, \ @@ -721,19 +722,20 @@ polygon(PyObject *self, PyObject *arg, PyObject *kwargs) Uint32 color; int *xlist = NULL, *ylist = NULL; int width = 0; /* Default width. */ + int border_radius = 0; /* Default border_radius. */ 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", "width", NULL}; + static char *keywords[] = {"surface", "color", "points", "width", "border_radius", NULL}; - if (!PyArg_ParseTupleAndKeywords(arg, kwargs, "O!OO|i", keywords, + if (!PyArg_ParseTupleAndKeywords(arg, kwargs, "O!OO|ii", keywords, &pgSurface_Type, &surfobj, &colorobj, - &points, &width)) { + &points, &width, &border_radius)) { return NULL; /* Exception already set. */ } - if (width) { + if (width && (border_radius==0)) { PyObject *ret = NULL; PyObject *args = Py_BuildValue("(OOiOi)", surfobj, colorobj, 1, points, width); @@ -810,7 +812,11 @@ polygon(PyObject *self, PyObject *arg, PyObject *kwargs) } if (length != 3) { + if(border_radius != 0){ + draw_round_polygon(surf, xlist, ylist, width, border_radius, length, color, drawn_area); + } else { draw_fillpoly(surf, xlist, ylist, length, color, drawn_area); + } } else { draw_filltri(surf, xlist, ylist, color, drawn_area); @@ -2818,6 +2824,115 @@ draw_round_rect(SDL_Surface *surf, int x1, int y1, int x2, int y2, int radius, } } +// Define a structure representing a point with x and y coordinates +typedef struct { + double x; + double y; +} Point; + +// Define a structure representing a circle with a center (Point) and a radius +typedef struct { + Point center; + double radius; +} Circle; + +// Define a structure representing a line segment with start and end points +typedef struct { + Point start; + Point end; +} Line; + +// Function to determine the side of point 'c' relative to the line formed by points 'a' and 'b' +// Returns 1 if on the left side, -1 if on the right side, and 0 if collinear +int side(Point a, Point b, Point c) { + double det = + (a.x * b.y + b.x * c.y + c.x * a.y) - (a.y * b.x + b.y * c.x + c.y * a.x); + if (det > 0) { + return 1; + } else if (det < 0) { + return -1; + } else { + return 0; + } +} + +// Function to find a parallel line to the line formed by 'pt1' and 'pt2' at a given distance +// Returns a Line struct representing the parallel line +Line find_parallel_line(Point pt1, Point pt2, Point pt3, int distance) { + // Calculate direction vector, normalize it, and find the perpendicular vector + Point direction_vector = {pt2.x - pt1.x, pt2.y - pt1.y}; + float magnitude = sqrt(direction_vector.x * direction_vector.x + direction_vector.y * direction_vector.y); + Point normalized_direction = {direction_vector.x / magnitude, direction_vector.y / magnitude}; + Point perpendicular_vector = {-normalized_direction.y, normalized_direction.x}; + // Adjust the perpendicular vector based on the side of 'pt3' relative to 'pt1' and 'pt2' + if (side(pt1, pt2, pt3) == -1) { + perpendicular_vector.x *= -1; + perpendicular_vector.y *= -1; + } + // Create an offset vector and generate the parallel line. + Point offset_vector = {perpendicular_vector.x * distance, perpendicular_vector.y * distance}; + Line parallel_line = {{pt1.x + offset_vector.x, pt1.y + offset_vector.y}, {pt2.x + offset_vector.x, pt2.y + offset_vector.y}}; + return parallel_line; +} + +// Function to project a point onto a line segment defined by 'segment_start' and 'segment_end' +Point project_point_onto_segment(Point point, Point segment_start, + Point segment_end) { + // Calculate vectors, find the parameter 't' for projection, and calculate the projection. + // Ensure 't' is within the valid range [0, 1]. + Point segment_vector; + segment_vector.x = segment_end.x - segment_start.x; + segment_vector.y = segment_end.y - segment_start.y; + + Point point_vector; + point_vector.x = point.x - segment_start.x; + point_vector.y = point.y - segment_start.y; + + double t = + (point_vector.x * segment_vector.x + point_vector.y * segment_vector.y) / + (segment_vector.x * segment_vector.x + + segment_vector.y * segment_vector.y); + t = fmax(0, fmin(1, t)); + + Point projection; + projection.x = segment_start.x + t * segment_vector.x; + projection.y = segment_start.y + t * segment_vector.y; + + return projection; +} + +// Function to find the intersection point of two line segments +// Returns the intersection point as a Point struct +Point intersection(Point line1_start, Point line1_end, Point line2_start, + Point line2_end) { + // Calculate coefficients for each line equation + double A1 = line1_end.y - line1_start.y; + double B1 = line1_start.x - line1_end.x; + double C1 = A1 * line1_start.x + B1 * line1_start.y; + + double A2 = line2_end.y - line2_start.y; + double B2 = line2_start.x - line2_end.x; + double C2 = A2 * line2_start.x + B2 * line2_start.y; + + // Check if the lines are parallel (det == 0) and handle the case + double det = A1 * B2 - A2 * B1; + if (det == 0) { + return (Point){0, 0}; + } else { + // Calculate the intersection point using Cramer's rule + double x = (B2 * C1 - B1 * C2) / det; + double y = (A1 * C2 - A2 * C1) / det; + return (Point){x, y}; + } +} + +// Function to calculate the angle (in radians) between a center point and another point +double angle(Point center, Point point) { + double x = point.x - center.x; + double y = point.y - center.y; + return -atan2(y, x); +} + /* List of python functions */ static PyMethodDef _draw_methods[] = { {"aaline", (PyCFunction)aaline, METH_VARARGS | METH_KEYWORDS, From f710d036902de52bdd2494c1472918238d807307 Mon Sep 17 00:00:00 2001 From: anas Date: Sun, 17 Dec 2023 22:01:35 +0100 Subject: [PATCH 02/17] Added draw_round_polygon and changed DOC_DRAW_POLYGON --- src_c/doc/draw_doc.h | 2 +- src_c/draw.c | 121 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src_c/doc/draw_doc.h b/src_c/doc/draw_doc.h index ec918d08d0..0be468d645 100644 --- a/src_c/doc/draw_doc.h +++ b/src_c/doc/draw_doc.h @@ -1,7 +1,7 @@ /* Auto generated file: with makeref.py . Docs go in docs/reST/ref/ . */ #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_POLYGON "polygon(surface, color, points) -> Rect\npolygon(surface, color, points, width=0, border_radius=0) -> Rect\ndraw a 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_ELLIPSE "ellipse(surface, color, rect) -> Rect\nellipse(surface, color, rect, width=0) -> Rect\ndraw an ellipse" #define DOC_DRAW_ARC "arc(surface, color, rect, start_angle, stop_angle) -> Rect\narc(surface, color, rect, start_angle, stop_angle, width=1) -> Rect\ndraw an elliptical arc" diff --git a/src_c/draw.c b/src_c/draw.c index a1dcc2c826..6c0ab1f146 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -84,6 +84,9 @@ static void draw_round_rect(SDL_Surface *surf, int x1, int y1, int x2, int y2, int radius, int width, Uint32 color, int top_left, int top_right, int bottom_left, int bottom_right, int *drawn_area); + static void +draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, int border_radius, + int num_points, Uint32 color, int *drawn_area); // validation of a draw color @@ -2933,6 +2936,124 @@ double angle(Point center, Point point) { return -atan2(y, x); } +// Define a function to draw a rounded polygon on an SDL surface +static void +draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, int border_radius, int num_points, Uint32 color, int *drawn_area) { + + // Define arrays to store path points and circle information + Point path[2 * num_points]; + Circle circles[num_points]; + + // Define an array to store original polygon points + Point points[num_points]; + + // Convert input coordinates to Point structures + for (int i = 0; i < num_points; i++) { + points[i].x = pts_x[i]; + points[i].y = pts_y[i]; + } + + // Iterate through each point in the polygon + for (int i = 0; i < num_points; i++) { + // Determine neighboring points for the current point + int a, b; + if (i == 0) { + a = num_points - 1; + b = 1; + } else if (i == (num_points - 1)) { + a = num_points - 2; + b = 0; + } else if (i == (num_points - 2)) { + a = num_points - 3; + b = num_points - 1; + } else { + a = i - 1; + b = i + 1; + } + + // Check if the border-radius can be applied to the current angle + if (side(points[a], points[i], points[b]) == 0) { + printf("ValueError: Border-radius cannot be applied to a flat angle.\n" + "Please ensure that the specified angle or curvature is within a valid range for border-radius drawing.\n"); + return; + } + + // Find parallel lines to the polygon sides at the current point + Line line1 = find_parallel_line(points[a], points[i], points[b], border_radius); + Line line2 = find_parallel_line(points[i], points[b], points[a], border_radius); + circles[i].center = intersection(line1.start, line1.end, line2.start, line2.end); + + // Project points onto the parallel lines + Point proj1 = + project_point_onto_segment(circles[i].center, points[a], points[i]); + Point proj2 = + project_point_onto_segment(circles[i].center, points[i], points[b]); + + // Check if the border-radius size is valid + if ((sqrt(pow(proj1.x - points[i].x, 2) + + pow(proj1.y - points[i].y, 2)) >= sqrt(pow(points[a].x - points[i].x, 2) + + pow(points[a].y - points[i].y, 2)) / 2) || + (sqrt(pow(proj2.x - points[i].x, 2) + + pow(proj2.y - points[i].y, 2)) >= sqrt(pow(points[b].x - points[i].x, 2) + + pow(points[b].y - points[i].y, 2)) / 2)) { + printf("ValueError: Border-radius size must be smaller\n"); + return; + } + + // Store projected points and circle radius in arrays + path[2 * i] = proj1; + path[2 * i + 1] = proj2; + + circles[i].radius = border_radius; + } + + // Iterate through the projected points to draw arcs and connecting lines + for (int i = 0; i < 2 * num_points; i += 2) { + Point pt1 = path[i % (2 * num_points)]; + Point pt2 = path[(i + 1) % (2 * num_points)]; + Point pt3 = path[(i + 2) % (2 * num_points)]; + Circle circle = circles[i / 2]; + + // Calculate start and end angles for the arc + double start_angle = angle(circle.center, pt1); + double end_angle = angle(circle.center, pt2); + + // Adjust angles based on the side of the polygon + if (side(pt1, pt2, pt3) == 1) { + double temp = start_angle; + start_angle = end_angle; + end_angle = temp; + } + + // Ensure the end angle is greater than the start angle (in radians) + if (end_angle < start_angle) { + // Angle is in radians + end_angle += 2 * M_PI; + } + + // Draw the arc and connecting line + draw_arc(surf, + circle.center.x, + circle.center.y, + circle.radius, + circle.radius, + width, + start_angle, + end_angle, + color, + drawn_area); + + draw_line_width(surf, + color, + pt2.x, + pt2.y, + pt3.x, + pt3.y, + width, + drawn_area); + } +} + /* List of python functions */ static PyMethodDef _draw_methods[] = { {"aaline", (PyCFunction)aaline, METH_VARARGS | METH_KEYWORDS, From ed7c02be2685793c73498f94d41b2b9930a0607b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CDahbani1=E2=80=9D?= Date: Mon, 18 Dec 2023 22:15:12 +0100 Subject: [PATCH 03/17] Comments added --- src_c/draw.c | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src_c/draw.c b/src_c/draw.c index 6c0ab1f146..4bcfc7c822 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -2848,13 +2848,18 @@ typedef struct { // Function to determine the side of point 'c' relative to the line formed by points 'a' and 'b' // Returns 1 if on the left side, -1 if on the right side, and 0 if collinear int side(Point a, Point b, Point c) { + // Calculate the determinant, a mathematical quantity used to determine the orientation of three points double det = (a.x * b.y + b.x * c.y + c.x * a.y) - (a.y * b.x + b.y * c.x + c.y * a.x); + // Check the sign of the determinant to determine the relative position of point 'c' if (det > 0) { return 1; + // If the determinant is positive, point 'c' is on the left side of the line } else if (det < 0) { + // If the determinant is negative, point 'c' is on the right side of the line return -1; } else { + // If the determinant is zero, points 'a', 'b', and 'c' are collinear return 0; } } @@ -2862,11 +2867,21 @@ int side(Point a, Point b, Point c) { // Function to find a parallel line to the line formed by 'pt1' and 'pt2' at a given distance // Returns a Line struct representing the parallel line Line find_parallel_line(Point pt1, Point pt2, Point pt3, int distance) { + // Calculate direction vector, normalize it, and find the perpendicular vector + + // Calculate direction vector from 'pt1' to 'pt2' Point direction_vector = {pt2.x - pt1.x, pt2.y - pt1.y}; + + // Calculate the magnitude of the direction vector float magnitude = sqrt(direction_vector.x * direction_vector.x + direction_vector.y * direction_vector.y); + + // Normalize the direction vector to get a unit vector Point normalized_direction = {direction_vector.x / magnitude, direction_vector.y / magnitude}; + + // Calculate the perpendicular vector by swapping x and y components and negating one of them Point perpendicular_vector = {-normalized_direction.y, normalized_direction.x}; + // Adjust the perpendicular vector based on the side of 'pt3' relative to 'pt1' and 'pt2' if (side(pt1, pt2, pt3) == -1) { perpendicular_vector.x *= -1; @@ -2874,15 +2889,19 @@ Line find_parallel_line(Point pt1, Point pt2, Point pt3, int distance) { } // Create an offset vector and generate the parallel line. Point offset_vector = {perpendicular_vector.x * distance, perpendicular_vector.y * distance}; + + // Generate the points for the parallel line based on the offset Line parallel_line = {{pt1.x + offset_vector.x, pt1.y + offset_vector.y}, {pt2.x + offset_vector.x, pt2.y + offset_vector.y}}; + + // Return the Line struct representing the parallel line return parallel_line; } + // Function to project a point onto a line segment defined by 'segment_start' and 'segment_end' Point project_point_onto_segment(Point point, Point segment_start, Point segment_end) { - // Calculate vectors, find the parameter 't' for projection, and calculate the projection. - // Ensure 't' is within the valid range [0, 1]. + // Calculate vectors representing the line segment and the vector from 'segment_start' to 'point' Point segment_vector; segment_vector.x = segment_end.x - segment_start.x; segment_vector.y = segment_end.y - segment_start.y; @@ -2891,19 +2910,25 @@ Point project_point_onto_segment(Point point, Point segment_start, point_vector.x = point.x - segment_start.x; point_vector.y = point.y - segment_start.y; + // Calculate the parameter 't' for the projection using the dot product double t = (point_vector.x * segment_vector.x + point_vector.y * segment_vector.y) / (segment_vector.x * segment_vector.x + segment_vector.y * segment_vector.y); + + // Ensure 't' is within the valid range [0, 1] to guarantee the projection lies on the line segment t = fmax(0, fmin(1, t)); + // Calculate the coordinates of the projected point using the parameter 't' Point projection; projection.x = segment_start.x + t * segment_vector.x; projection.y = segment_start.y + t * segment_vector.y; + // Return the projected point on the line segment return projection; } + // Function to find the intersection point of two line segments // Returns the intersection point as a Point struct Point intersection(Point line1_start, Point line1_end, Point line2_start, @@ -2919,6 +2944,7 @@ Point intersection(Point line1_start, Point line1_end, Point line2_start, // Check if the lines are parallel (det == 0) and handle the case double det = A1 * B2 - A2 * B1; + // If the lines are parallel (det == 0), they do not intersect, return a default Point if (det == 0) { return (Point){0, 0}; } else { @@ -2931,8 +2957,12 @@ Point intersection(Point line1_start, Point line1_end, Point line2_start, // Function to calculate the angle (in radians) between a center point and another point double angle(Point center, Point point) { + // Calculate the differences in x and y coordinates between the two points double x = point.x - center.x; double y = point.y - center.y; + // Use atan2 to calculate the angle formed by the vector from the center to the point + // The angle is measured counterclockwise from the positive x-axis + // Note: The negative sign is used to ensure the angle is measured clockwise, which is more common in Cartesian coordinates. return -atan2(y, x); } @@ -2990,6 +3020,9 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, int bor project_point_onto_segment(circles[i].center, points[i], points[b]); // Check if the border-radius size is valid + + // Compare the distance between the projected points and the corresponding endpoints + // If the distance is greater than or equal to half of the corresponding line segment, it is considered invalid if ((sqrt(pow(proj1.x - points[i].x, 2) + pow(proj1.y - points[i].y, 2)) >= sqrt(pow(points[a].x - points[i].x, 2) + pow(points[a].y - points[i].y, 2)) / 2) || From 16e6ac577ea58814d8c9ffdf28f4a2e289abdc53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CDahbani1=E2=80=9D?= Date: Tue, 19 Dec 2023 00:23:59 +0100 Subject: [PATCH 04/17] Comments added --- src_c/draw.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src_c/draw.c b/src_c/draw.c index 4bcfc7c822..4b8fe8e87d 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -2865,6 +2865,7 @@ int side(Point a, Point b, Point c) { } // Function to find a parallel line to the line formed by 'pt1' and 'pt2' at a given distance +// 'pt3' is used to determin in which side of the line, the parallel line should be drawn // Returns a Line struct representing the parallel line Line find_parallel_line(Point pt1, Point pt2, Point pt3, int distance) { @@ -2901,7 +2902,8 @@ Line find_parallel_line(Point pt1, Point pt2, Point pt3, int distance) { // Function to project a point onto a line segment defined by 'segment_start' and 'segment_end' Point project_point_onto_segment(Point point, Point segment_start, Point segment_end) { - // Calculate vectors representing the line segment and the vector from 'segment_start' to 'point' + // t is a parameter that represents the position of the projected point along the line segment defined by segment_start and segment_end. + // Calculate vectors, find the parameter 't' for projection, and calculate the projection. Point segment_vector; segment_vector.x = segment_end.x - segment_start.x; segment_vector.y = segment_end.y - segment_start.y; @@ -2916,7 +2918,8 @@ Point project_point_onto_segment(Point point, Point segment_start, (segment_vector.x * segment_vector.x + segment_vector.y * segment_vector.y); - // Ensure 't' is within the valid range [0, 1] to guarantee the projection lies on the line segment + // Ensure 't' is within the valid range [0, 1] to make sure that the projection of the point onto the line segment lies specifically within the boundaries of the segment + // See line 2906 t = fmax(0, fmin(1, t)); // Calculate the coordinates of the projected point using the parameter 't' @@ -2949,6 +2952,7 @@ Point intersection(Point line1_start, Point line1_end, Point line2_start, return (Point){0, 0}; } else { // Calculate the intersection point using Cramer's rule + // The lines never intersect out of the range given because of the condition limiting the value of boarder radius double x = (B2 * C1 - B1 * C2) / det; double y = (A1 * C2 - A2 * C1) / det; return (Point){x, y}; From 646ce2fc07ed9eb206261a61daccb149407c9220 Mon Sep 17 00:00:00 2001 From: anas Date: Tue, 19 Dec 2023 00:32:27 +0100 Subject: [PATCH 05/17] Fixed error handling, Added dynamic array handling with malloc --- src_c/draw.c | 178 ++++++++++++++++++++++++++------------------------- 1 file changed, 90 insertions(+), 88 deletions(-) diff --git a/src_c/draw.c b/src_c/draw.c index 4b8fe8e87d..1e4bab77bf 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -84,7 +84,7 @@ static void draw_round_rect(SDL_Surface *surf, int x1, int y1, int x2, int y2, int radius, int width, Uint32 color, int top_left, int top_right, int bottom_left, int bottom_right, int *drawn_area); - static void +static void draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, int border_radius, int num_points, Uint32 color, int *drawn_area); @@ -738,7 +738,7 @@ polygon(PyObject *self, PyObject *arg, PyObject *kwargs) return NULL; /* Exception already set. */ } - if (width && (border_radius==0)) { + if (width && (border_radius <= 0)) { PyObject *ret = NULL; PyObject *args = Py_BuildValue("(OOiOi)", surfobj, colorobj, 1, points, width); @@ -814,11 +814,17 @@ polygon(PyObject *self, PyObject *arg, PyObject *kwargs) return RAISE(PyExc_RuntimeError, "error locking surface"); } - if (length != 3) { - if(border_radius != 0){ - draw_round_polygon(surf, xlist, ylist, width, border_radius, length, color, drawn_area); - } else { - draw_fillpoly(surf, xlist, ylist, length, color, drawn_area); + if (length != 3) { + if ((border_radius > 0) && (width > 0)) { + int err; + err = draw_round_polygon(surf, xlist, ylist, width, border_radius, + length, color, drawn_area); + if (err == -1) { + return NULL; + } + } + else { + draw_fillpoly(surf, xlist, ylist, length, color, drawn_area); } } else { @@ -2827,32 +2833,30 @@ draw_round_rect(SDL_Surface *surf, int x1, int y1, int x2, int y2, int radius, } } -// Define a structure representing a point with x and y coordinates typedef struct { double x; double y; } Point; -// Define a structure representing a circle with a center (Point) and a radius typedef struct { Point center; double radius; } Circle; -// Define a structure representing a line segment with start and end points typedef struct { Point start; Point end; } Line; -// Function to determine the side of point 'c' relative to the line formed by points 'a' and 'b' -// Returns 1 if on the left side, -1 if on the right side, and 0 if collinear -int side(Point a, Point b, Point c) { - // Calculate the determinant, a mathematical quantity used to determine the orientation of three points - double det = - (a.x * b.y + b.x * c.y + c.x * a.y) - (a.y * b.x + b.y * c.x + c.y * a.x); - // Check the sign of the determinant to determine the relative position of point 'c' - if (det > 0) { +// Function to determine the side of point 'c' relative to the line formed by +// points 'a' and 'b' Returns 1 if on the left side, -1 if on the right side, +// and 0 if collinear +int +side(Point a, Point b, Point c) +{ + double det = (a.x * b.y + b.x * c.y + c.x * a.y) - + (a.y * b.x + b.y * c.x + c.y * a.x); + if (det > 0) { return 1; // If the determinant is positive, point 'c' is on the left side of the line } else if (det < 0) { @@ -2971,15 +2975,34 @@ double angle(Point center, Point point) { } // Define a function to draw a rounded polygon on an SDL surface -static void -draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, int border_radius, int num_points, Uint32 color, int *drawn_area) { - +static int +draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, + int border_radius, int num_points, Uint32 color, + int *drawn_area) +{ // Define arrays to store path points and circle information - Point path[2 * num_points]; - Circle circles[num_points]; + Point *path; + Circle *circles; + Point *points; - // Define an array to store original polygon points - Point points[num_points]; + path = (Point *)malloc(2 * num_points * sizeof(Point)); + circles = (Circle *)malloc(num_points * sizeof(Circle)); + points = (Point *)malloc(num_points * sizeof(Point)); + + if (points == NULL || circles == NULL || path == NULL) { + if (points) { + free(points); + } + if (circles) { + free(circles); + } + if (path) { + free(path); + } + PyErr_SetString(PyExc_MemoryError, + "cannot allocate memory to draw polygon"); + return -1; + } // Convert input coordinates to Point structures for (int i = 0; i < num_points; i++) { @@ -2987,54 +3010,50 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, int bor points[i].y = pts_y[i]; } - // Iterate through each point in the polygon - for (int i = 0; i < num_points; i++) { - // Determine neighboring points for the current point - int a, b; - if (i == 0) { - a = num_points - 1; - b = 1; - } else if (i == (num_points - 1)) { - a = num_points - 2; - b = 0; - } else if (i == (num_points - 2)) { - a = num_points - 3; - b = num_points - 1; - } else { - a = i - 1; - b = i + 1; - } + for (int i = 0; i < num_points; i++) { + int a = (i - 1 + num_points) % + (num_points); // index of the point before index i + int b = (i + 1) % (num_points); // index of the point after index i // Check if the border-radius can be applied to the current angle +// it can not be applied to a flat angle(if the three points are + // aligned) if (side(points[a], points[i], points[b]) == 0) { - printf("ValueError: Border-radius cannot be applied to a flat angle.\n" - "Please ensure that the specified angle or curvature is within a valid range for border-radius drawing.\n"); - return; + PyErr_SetString( + PyExc_ValueError, + "Border-radius cannot be applied to a flat angle.\nPlease " + "ensure that the specified angle or curvature is within a " + "valid range for border-radius drawing.\n"); + return -1; } // Find parallel lines to the polygon sides at the current point - Line line1 = find_parallel_line(points[a], points[i], points[b], border_radius); - Line line2 = find_parallel_line(points[i], points[b], points[a], border_radius); - circles[i].center = intersection(line1.start, line1.end, line2.start, line2.end); - - // Project points onto the parallel lines - Point proj1 = - project_point_onto_segment(circles[i].center, points[a], points[i]); - Point proj2 = - project_point_onto_segment(circles[i].center, points[i], points[b]); + Line line1 = + find_parallel_line(points[a], points[i], points[b], border_radius); + Line line2 = + find_parallel_line(points[i], points[b], points[a], border_radius); + circles[i].center = + intersection(line1.start, line1.end, line2.start, line2.end); + + Point proj1 = project_point_onto_segment(circles[i].center, points[a], + points[i]); + Point proj2 = project_point_onto_segment(circles[i].center, points[i], + points[b]); // Check if the border-radius size is valid - - // Compare the distance between the projected points and the corresponding endpoints - // If the distance is greater than or equal to half of the corresponding line segment, it is considered invalid if ((sqrt(pow(proj1.x - points[i].x, 2) + - pow(proj1.y - points[i].y, 2)) >= sqrt(pow(points[a].x - points[i].x, 2) + - pow(points[a].y - points[i].y, 2)) / 2) || + pow(proj1.y - points[i].y, 2)) >= + sqrt(pow(points[a].x - points[i].x, 2) + + pow(points[a].y - points[i].y, 2)) / + 2) || (sqrt(pow(proj2.x - points[i].x, 2) + - pow(proj2.y - points[i].y, 2)) >= sqrt(pow(points[b].x - points[i].x, 2) + - pow(points[b].y - points[i].y, 2)) / 2)) { - printf("ValueError: Border-radius size must be smaller\n"); - return; + pow(proj2.y - points[i].y, 2)) >= + sqrt(pow(points[b].x - points[i].x, 2) + + pow(points[b].y - points[i].y, 2)) / + 2)) { + PyErr_SetString(PyExc_ValueError, + "Border-radius size must be smaller\n"); + return -1; } // Store projected points and circle radius in arrays @@ -3051,8 +3070,7 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, int bor Point pt3 = path[(i + 2) % (2 * num_points)]; Circle circle = circles[i / 2]; - // Calculate start and end angles for the arc - double start_angle = angle(circle.center, pt1); + double start_angle = angle(circle.center, pt1); double end_angle = angle(circle.center, pt2); // Adjust angles based on the side of the polygon @@ -3062,33 +3080,17 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, int bor end_angle = temp; } - // Ensure the end angle is greater than the start angle (in radians) - if (end_angle < start_angle) { - // Angle is in radians - end_angle += 2 * M_PI; - } - - // Draw the arc and connecting line - draw_arc(surf, - circle.center.x, - circle.center.y, - circle.radius, - circle.radius, - width, - start_angle, - end_angle, - color, + draw_arc(surf, circle.center.x, circle.center.y, circle.radius, + circle.radius, width, start_angle, end_angle, color, drawn_area); - draw_line_width(surf, - color, - pt2.x, - pt2.y, - pt3.x, - pt3.y, - width, + draw_line_width(surf, color, pt2.x, pt2.y, pt3.x, pt3.y, width, drawn_area); } +free(path); + free(circles); + free(points); + return 0; } /* List of python functions */ From 39fae054febd0d6eda6d681c4788753cb458c603 Mon Sep 17 00:00:00 2001 From: anas Date: Tue, 19 Dec 2023 00:43:27 +0100 Subject: [PATCH 06/17] Updated documentation --- docs/reST/ref/draw.rst | 8 ++- src_c/draw.c | 159 ++++++++++++++++++++++++----------------- 2 files changed, 99 insertions(+), 68 deletions(-) diff --git a/docs/reST/ref/draw.rst b/docs/reST/ref/draw.rst index ab88f06ddd..874a20e2c5 100644 --- a/docs/reST/ref/draw.rst +++ b/docs/reST/ref/draw.rst @@ -103,7 +103,7 @@ object around the draw calls (see :func:`pygame.Surface.lock` and | :sl:`draw a polygon` | :sg:`polygon(surface, color, points) -> Rect` - | :sg:`polygon(surface, color, points, width=0) -> Rect` + | :sg:`polygon(surface, color, points, width=0, border_radius=0) -> Rect` Draws a polygon on the given surface. @@ -129,6 +129,8 @@ object around the draw calls (see :func:`pygame.Surface.lock` and outside the original boundary of the polygon. For more details on how the thickness for edge lines grow, refer to the ``width`` notes of the :func:`pygame.draw.line` function. + :param int border_radius: (optional, default: 0) determines the radius for rounded corners of the polygon. + Only active when both ``width > 0`` and ``border_radius > 0``. :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 @@ -136,7 +138,9 @@ object around the draw calls (see :func:`pygame.Surface.lock` and height will be 0 :rtype: Rect - :raises ValueError: if ``len(points) < 3`` (must have at least 3 points) + :raises ValueError: if ``len(points) < 3`` (must have at least 3 points), + or if three of the given points provided in inputs are aligned, + or if the radius is very large in relation to the size of the adjacent edges. :raises TypeError: if ``points`` is not a sequence or ``points`` does not contain number pairs diff --git a/src_c/draw.c b/src_c/draw.c index 1e4bab77bf..60d85d925e 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -84,10 +84,10 @@ static void draw_round_rect(SDL_Surface *surf, int x1, int y1, int x2, int y2, int radius, int width, Uint32 color, int top_left, int top_right, int bottom_left, int bottom_right, int *drawn_area); -static void -draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, int border_radius, - int num_points, Uint32 color, int *drawn_area); - +static int +draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, + int border_radius, int num_points, Uint32 color, + int *drawn_area); // validation of a draw color #define CHECK_LOAD_COLOR(colorobj) \ @@ -724,13 +724,14 @@ polygon(PyObject *self, PyObject *arg, PyObject *kwargs) SDL_Surface *surf = NULL; Uint32 color; int *xlist = NULL, *ylist = NULL; - int width = 0; /* Default width. */ + int width = 0; /* Default width. */ int border_radius = 0; /* Default border_radius. */ 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", "width", "border_radius", NULL}; + static char *keywords[] = {"surface", "color", "points", + "width", "border_radius", NULL}; if (!PyArg_ParseTupleAndKeywords(arg, kwargs, "O!OO|ii", keywords, &pgSurface_Type, &surfobj, &colorobj, @@ -814,7 +815,7 @@ polygon(PyObject *self, PyObject *arg, PyObject *kwargs) return RAISE(PyExc_RuntimeError, "error locking surface"); } - if (length != 3) { + if (length != 3) { if ((border_radius > 0) && (width > 0)) { int err; err = draw_round_polygon(surf, xlist, ylist, width, border_radius, @@ -2856,58 +2857,75 @@ side(Point a, Point b, Point c) { double det = (a.x * b.y + b.x * c.y + c.x * a.y) - (a.y * b.x + b.y * c.x + c.y * a.x); - if (det > 0) { + if (det > 0) { return 1; - // If the determinant is positive, point 'c' is on the left side of the line - } else if (det < 0) { - // If the determinant is negative, point 'c' is on the right side of the line + // If the determinant is positive, point 'c' is on the left side of the + // line + } + else if (det < 0) { + // If the determinant is negative, point 'c' is on the right side of + // the line return -1; - } else { + } + else { // If the determinant is zero, points 'a', 'b', and 'c' are collinear return 0; } } -// Function to find a parallel line to the line formed by 'pt1' and 'pt2' at a given distance -// 'pt3' is used to determin in which side of the line, the parallel line should be drawn -// Returns a Line struct representing the parallel line -Line find_parallel_line(Point pt1, Point pt2, Point pt3, int distance) { - - // Calculate direction vector, normalize it, and find the perpendicular vector - +// Function to find a parallel line to the line formed by 'pt1' and 'pt2' at a +// given distance 'pt3' is used to determin in which side of the line, the +// parallel line should be drawn Returns a Line struct representing the +// parallel line +Line +find_parallel_line(Point pt1, Point pt2, Point pt3, int distance) +{ + // Calculate direction vector, normalize it, and find the perpendicular + // vector + // Calculate direction vector from 'pt1' to 'pt2' Point direction_vector = {pt2.x - pt1.x, pt2.y - pt1.y}; // Calculate the magnitude of the direction vector - float magnitude = sqrt(direction_vector.x * direction_vector.x + direction_vector.y * direction_vector.y); - + float magnitude = sqrt(direction_vector.x * direction_vector.x + + direction_vector.y * direction_vector.y); + // Normalize the direction vector to get a unit vector - Point normalized_direction = {direction_vector.x / magnitude, direction_vector.y / magnitude}; - - // Calculate the perpendicular vector by swapping x and y components and negating one of them - Point perpendicular_vector = {-normalized_direction.y, normalized_direction.x}; - - // Adjust the perpendicular vector based on the side of 'pt3' relative to 'pt1' and 'pt2' + Point normalized_direction = {direction_vector.x / magnitude, + direction_vector.y / magnitude}; + + // Calculate the perpendicular vector by swapping x and y components and + // negating one of them + Point perpendicular_vector = {-normalized_direction.y, + normalized_direction.x}; + + // Adjust the perpendicular vector based on the side of 'pt3' relative to + // 'pt1' and 'pt2' if (side(pt1, pt2, pt3) == -1) { perpendicular_vector.x *= -1; perpendicular_vector.y *= -1; } // Create an offset vector and generate the parallel line. - Point offset_vector = {perpendicular_vector.x * distance, perpendicular_vector.y * distance}; - + Point offset_vector = {perpendicular_vector.x * distance, + perpendicular_vector.y * distance}; + // Generate the points for the parallel line based on the offset - Line parallel_line = {{pt1.x + offset_vector.x, pt1.y + offset_vector.y}, {pt2.x + offset_vector.x, pt2.y + offset_vector.y}}; - + Line parallel_line = {{pt1.x + offset_vector.x, pt1.y + offset_vector.y}, + {pt2.x + offset_vector.x, pt2.y + offset_vector.y}}; + // Return the Line struct representing the parallel line return parallel_line; } - -// Function to project a point onto a line segment defined by 'segment_start' and 'segment_end' -Point project_point_onto_segment(Point point, Point segment_start, - Point segment_end) { - // t is a parameter that represents the position of the projected point along the line segment defined by segment_start and segment_end. - // Calculate vectors, find the parameter 't' for projection, and calculate the projection. +// Function to project a point onto a line segment defined by 'segment_start' +// and 'segment_end' +Point +project_point_onto_segment(Point point, Point segment_start, Point segment_end) +{ + // t is a parameter that represents the position of the projected point + // along the line segment defined by segment_start and segment_end. + // Calculate vectors, find the parameter 't' for projection, and calculate + // the projection. Point segment_vector; segment_vector.x = segment_end.x - segment_start.x; segment_vector.y = segment_end.y - segment_start.y; @@ -2917,13 +2935,14 @@ Point project_point_onto_segment(Point point, Point segment_start, point_vector.y = point.y - segment_start.y; // Calculate the parameter 't' for the projection using the dot product - double t = - (point_vector.x * segment_vector.x + point_vector.y * segment_vector.y) / - (segment_vector.x * segment_vector.x + - segment_vector.y * segment_vector.y); - - // Ensure 't' is within the valid range [0, 1] to make sure that the projection of the point onto the line segment lies specifically within the boundaries of the segment - // See line 2906 + double t = (point_vector.x * segment_vector.x + + point_vector.y * segment_vector.y) / + (segment_vector.x * segment_vector.x + + segment_vector.y * segment_vector.y); + + // Ensure 't' is within the valid range [0, 1] to make sure that the + // projection of the point onto the line segment lies specifically within + // the boundaries of the segment See line 2906 t = fmax(0, fmin(1, t)); // Calculate the coordinates of the projected point using the parameter 't' @@ -2935,11 +2954,12 @@ Point project_point_onto_segment(Point point, Point segment_start, return projection; } - // Function to find the intersection point of two line segments // Returns the intersection point as a Point struct -Point intersection(Point line1_start, Point line1_end, Point line2_start, - Point line2_end) { +Point +intersection(Point line1_start, Point line1_end, Point line2_start, + Point line2_end) +{ // Calculate coefficients for each line equation double A1 = line1_end.y - line1_start.y; double B1 = line1_start.x - line1_end.x; @@ -2951,26 +2971,33 @@ Point intersection(Point line1_start, Point line1_end, Point line2_start, // Check if the lines are parallel (det == 0) and handle the case double det = A1 * B2 - A2 * B1; - // If the lines are parallel (det == 0), they do not intersect, return a default Point + // If the lines are parallel (det == 0), they do not intersect, return a + // default Point if (det == 0) { return (Point){0, 0}; - } else { + } + else { // Calculate the intersection point using Cramer's rule - // The lines never intersect out of the range given because of the condition limiting the value of boarder radius + // The lines never intersect out of the range given because of the + // condition limiting the value of boarder radius double x = (B2 * C1 - B1 * C2) / det; double y = (A1 * C2 - A2 * C1) / det; return (Point){x, y}; } } -// Function to calculate the angle (in radians) between a center point and another point -double angle(Point center, Point point) { +// Function to calculate the angle (in radians) between a center point and +// another point +double +angle(Point center, Point point) +{ // Calculate the differences in x and y coordinates between the two points double x = point.x - center.x; double y = point.y - center.y; - // Use atan2 to calculate the angle formed by the vector from the center to the point - // The angle is measured counterclockwise from the positive x-axis - // Note: The negative sign is used to ensure the angle is measured clockwise, which is more common in Cartesian coordinates. + // Use atan2 to calculate the angle formed by the vector from the center to + // the point The angle is measured counterclockwise from the positive + // x-axis Note: The negative sign is used to ensure the angle is measured + // clockwise, which is more common in Cartesian coordinates. return -atan2(y, x); } @@ -3010,13 +3037,13 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, points[i].y = pts_y[i]; } - for (int i = 0; i < num_points; i++) { + for (int i = 0; i < num_points; i++) { int a = (i - 1 + num_points) % (num_points); // index of the point before index i int b = (i + 1) % (num_points); // index of the point after index i // Check if the border-radius can be applied to the current angle -// it can not be applied to a flat angle(if the three points are + // it can not be applied to a flat angle(if the three points are // aligned) if (side(points[a], points[i], points[b]) == 0) { PyErr_SetString( @@ -3042,14 +3069,14 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, // Check if the border-radius size is valid if ((sqrt(pow(proj1.x - points[i].x, 2) + - pow(proj1.y - points[i].y, 2)) >= + pow(proj1.y - points[i].y, 2)) >= sqrt(pow(points[a].x - points[i].x, 2) + - pow(points[a].y - points[i].y, 2)) / - 2) || + pow(points[a].y - points[i].y, 2)) / + 2) || (sqrt(pow(proj2.x - points[i].x, 2) + - pow(proj2.y - points[i].y, 2)) >= + pow(proj2.y - points[i].y, 2)) >= sqrt(pow(points[b].x - points[i].x, 2) + - pow(points[b].y - points[i].y, 2)) / + pow(points[b].y - points[i].y, 2)) / 2)) { PyErr_SetString(PyExc_ValueError, "Border-radius size must be smaller\n"); @@ -3070,7 +3097,7 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, Point pt3 = path[(i + 2) % (2 * num_points)]; Circle circle = circles[i / 2]; - double start_angle = angle(circle.center, pt1); + double start_angle = angle(circle.center, pt1); double end_angle = angle(circle.center, pt2); // Adjust angles based on the side of the polygon @@ -3081,13 +3108,13 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, } draw_arc(surf, circle.center.x, circle.center.y, circle.radius, - circle.radius, width, start_angle, end_angle, color, - drawn_area); + circle.radius, width, start_angle, end_angle, color, + drawn_area); draw_line_width(surf, color, pt2.x, pt2.y, pt3.x, pt3.y, width, drawn_area); } -free(path); + free(path); free(circles); free(points); return 0; From 61f5b99c74051a0893487463989a03209eb39209 Mon Sep 17 00:00:00 2001 From: anas Date: Tue, 19 Dec 2023 02:41:51 +0100 Subject: [PATCH 07/17] Updated stub --- buildconfig/stubs/pygame/draw.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/buildconfig/stubs/pygame/draw.pyi b/buildconfig/stubs/pygame/draw.pyi index 7a47a0b0d5..d7a6ed64aa 100644 --- a/buildconfig/stubs/pygame/draw.pyi +++ b/buildconfig/stubs/pygame/draw.pyi @@ -19,6 +19,7 @@ def polygon( color: ColorValue, points: Sequence[Coordinate], width: int = 0, + border_radius: int = 0, ) -> Rect: ... def circle( surface: Surface, From 0e489dd23e4ecbdd450b3205a2d2e6a88618cdd4 Mon Sep 17 00:00:00 2001 From: anas Date: Tue, 19 Dec 2023 13:58:32 +0100 Subject: [PATCH 08/17] Resolved type conversion --- src_c/draw.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src_c/draw.c b/src_c/draw.c index 60d85d925e..b2e869657a 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -819,7 +819,7 @@ polygon(PyObject *self, PyObject *arg, PyObject *kwargs) if ((border_radius > 0) && (width > 0)) { int err; err = draw_round_polygon(surf, xlist, ylist, width, border_radius, - length, color, drawn_area); + (int)length, color, drawn_area); if (err == -1) { return NULL; } @@ -2887,8 +2887,8 @@ find_parallel_line(Point pt1, Point pt2, Point pt3, int distance) Point direction_vector = {pt2.x - pt1.x, pt2.y - pt1.y}; // Calculate the magnitude of the direction vector - float magnitude = sqrt(direction_vector.x * direction_vector.x + - direction_vector.y * direction_vector.y); + double magnitude = sqrt(direction_vector.x * direction_vector.x + + direction_vector.y * direction_vector.y); // Normalize the direction vector to get a unit vector Point normalized_direction = {direction_vector.x / magnitude, @@ -3107,12 +3107,12 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, end_angle = temp; } - draw_arc(surf, circle.center.x, circle.center.y, circle.radius, - circle.radius, width, start_angle, end_angle, color, - drawn_area); + draw_arc(surf, round(circle.center.x), round(circle.center.y), + round(circle.radius), round(circle.radius), width, + start_angle, end_angle, color, drawn_area); - draw_line_width(surf, color, pt2.x, pt2.y, pt3.x, pt3.y, width, - drawn_area); + draw_line_width(surf, color, round(pt2.x), round(pt2.y), round(pt3.x), + round(pt3.y), width, drawn_area); } free(path); free(circles); From ac168b187046ff44edd770aa729ad8c5d168be59 Mon Sep 17 00:00:00 2001 From: anas Date: Tue, 19 Dec 2023 16:38:32 +0100 Subject: [PATCH 09/17] Resolved integer conversion --- src_c/draw.c | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src_c/draw.c b/src_c/draw.c index b2e869657a..51a743dbde 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -30,7 +30,7 @@ #include "doc/draw_doc.h" #include - +#define sametype(A, B) _Generic(*((A *)0), B: true, default: false) #include #ifndef M_PI @@ -2841,7 +2841,7 @@ typedef struct { typedef struct { Point center; - double radius; + int radius; } Circle; typedef struct { @@ -3107,12 +3107,19 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, end_angle = temp; } - draw_arc(surf, round(circle.center.x), round(circle.center.y), - round(circle.radius), round(circle.radius), width, - start_angle, end_angle, color, drawn_area); + int circle_center_x = round(circle.center.x); + int circle_center_y = round(circle.center.y); + int pt2_x = round(pt2.x); + int pt2_y = round(pt2.y); + int pt3_x = round(pt3.x); + int pt3_y = round(pt3.y); + + draw_arc(surf, circle_center_x, circle_center_y, circle.radius, + circle.radius, width, start_angle, end_angle, color, + drawn_area); - draw_line_width(surf, color, round(pt2.x), round(pt2.y), round(pt3.x), - round(pt3.y), width, drawn_area); + draw_line_width(surf, color, pt2_x, pt2_y, pt3_x, pt3_y, width, + drawn_area); } free(path); free(circles); From cc236362d7846926945f75414aae92092bc52de3 Mon Sep 17 00:00:00 2001 From: anas Date: Tue, 19 Dec 2023 16:45:33 +0100 Subject: [PATCH 10/17] Removed unnecessary include for test --- src_c/draw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src_c/draw.c b/src_c/draw.c index 51a743dbde..56bce84abe 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -30,7 +30,7 @@ #include "doc/draw_doc.h" #include -#define sametype(A, B) _Generic(*((A *)0), B: true, default: false) + #include #ifndef M_PI From faf9922cbf55453b45bbc5289cc8c05e77538fa5 Mon Sep 17 00:00:00 2001 From: anas Date: Tue, 19 Dec 2023 22:32:09 +0100 Subject: [PATCH 11/17] Added tests --- test/draw_test.py | 92 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 68 insertions(+), 24 deletions(-) diff --git a/test/draw_test.py b/test/draw_test.py index 4429740b28..d8572debea 100644 --- a/test/draw_test.py +++ b/test/draw_test.py @@ -3645,12 +3645,20 @@ def setUp(self): def test_polygon__args(self): """Ensures draw polygon accepts the correct args.""" bounds_rect = self.draw_polygon( - pygame.Surface((3, 3)), (0, 10, 0, 50), ((0, 0), (1, 1), (2, 2)), 1 + pygame.Surface((3, 3)), (0, 10, 0, 50), ((0, 0), (1, 1), (2, 2)), 1, 1 ) self.assertIsInstance(bounds_rect, pygame.Rect) - def test_polygon__args_without_width(self): + def test_polygon__args_without_radius(self): + """Ensures draw polygon accepts the args without a width.""" + bounds_rect = self.draw_polygon( + pygame.Surface((2, 2)), (0, 0, 0, 50), ((0, 0), (1, 1), (2, 2)), 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_polygon__args_without_width_nor_radius(self): """Ensures draw polygon accepts the args without a width.""" bounds_rect = self.draw_polygon( pygame.Surface((2, 2)), (0, 0, 0, 50), ((0, 0), (1, 1), (2, 2)) @@ -3666,6 +3674,7 @@ def test_polygon__kwargs(self): color = pygame.Color("yellow") points = ((0, 0), (1, 1), (2, 2)) kwargs_list = [ + {"surface": surface, "color": color, "points": points, "width": 1, "border_radius": 1}, {"surface": surface, "color": color, "points": points, "width": 1}, {"surface": surface, "color": color, "points": points}, ] @@ -3680,6 +3689,7 @@ def test_polygon__kwargs_order_independent(self): bounds_rect = self.draw_polygon( color=(10, 20, 30), surface=pygame.Surface((3, 2)), + border_radius=1, width=0, points=((0, 1), (1, 2), (2, 3)), ) @@ -3707,6 +3717,7 @@ def test_polygon__kwargs_missing(self): "color": pygame.Color("red"), "points": ((2, 1), (2, 2), (2, 3)), "width": 1, + "border_radius": 1, } for name in ("points", "color", "surface"): @@ -3722,6 +3733,10 @@ def test_polygon__arg_invalid_types(self): color = pygame.Color("blue") points = ((0, 1), (1, 2), (1, 3)) + with self.assertRaises(TypeError): + # Invalid border_radius. + bounds_rect = self.draw_polygon(surface, color, points, 1, "1") + with self.assertRaises(TypeError): # Invalid width. bounds_rect = self.draw_polygon(surface, color, points, "1") @@ -3744,27 +3759,44 @@ def test_polygon__kwarg_invalid_types(self): color = pygame.Color("green") points = ((0, 0), (1, 0), (2, 0)) width = 1 + border_radius = 1 kwargs_list = [ { "surface": pygame.Surface, # Invalid surface. "color": color, "points": points, "width": width, + "border_radius": border_radius, }, { "surface": surface, - "color": 2.3, # Invalid color. + "color": 2.3, # Invalid color. "points": points, "width": width, + "border_radius": border_radius, }, { "surface": surface, "color": color, "points": ((1,), (1,), (1,)), # Invalid points. "width": width, + "border_radius": border_radius, }, - {"surface": surface, "color": color, "points": points, "width": 1.2}, - ] # Invalid width. + { + "surface": surface, + "color": color, + "points": points, + "width": 1.2, # Invalid width. + "border_radius": border_radius, + }, + { + "surface": surface, + "color": color, + "points": points, + "width": width, + "border_radius": 2.3, # Invalid border_radius. + }, + ] for kwargs in kwargs_list: with self.assertRaises(TypeError): @@ -3781,9 +3813,10 @@ def test_polygon__kwarg_invalid_name(self): "color": color, "points": points, "width": 1, + "border_radius": 1, "invalid": 1, }, - {"surface": surface, "color": color, "points": points, "invalid": 1}, + {"surface": surface, "color": color, "points": points, "invalid": 1, "border_radius": 1}, ] for kwargs in kwargs_list: @@ -3796,9 +3829,10 @@ def test_polygon__args_and_kwargs(self): color = (255, 255, 0, 0) points = ((0, 1), (1, 2), (2, 3)) width = 0 - kwargs = {"surface": surface, "color": color, "points": points, "width": width} + border_radius = 0 + kwargs = {"surface": surface, "color": color, "points": points, "width": width, "border_radius": border_radius} - for name in ("surface", "color", "points", "width"): + for name in ("surface", "color", "points", "width", "border_radius"): kwargs.pop(name) if "surface" == name: @@ -3807,11 +3841,13 @@ def test_polygon__args_and_kwargs(self): bounds_rect = self.draw_polygon(surface, color, **kwargs) elif "points" == name: bounds_rect = self.draw_polygon(surface, color, points, **kwargs) - else: + elif "width" == name: bounds_rect = self.draw_polygon(surface, color, points, width, **kwargs) + else: + bounds_rect = self.draw_polygon(surface, color, points, width, border_radius, **kwargs) self.assertIsInstance(bounds_rect, pygame.Rect) - + def test_polygon__valid_width_values(self): """Ensures draw polygon accepts different width values.""" surface_color = pygame.Color("white") @@ -3822,6 +3858,7 @@ def test_polygon__valid_width_values(self): "color": color, "points": ((1, 1), (2, 1), (2, 2), (1, 2)), "width": None, + "border_radius": 0, } pos = kwargs["points"][0] @@ -3845,6 +3882,7 @@ def test_polygon__valid_points_format(self): "color": expected_color, "points": None, "width": 0, + "border_radius": 0, } # The point type can be a tuple/list/Vector2. @@ -3885,6 +3923,7 @@ def test_polygon__invalid_points_formats(self): "color": pygame.Color("red"), "points": None, "width": 0, + "border_radius": 0, } points_fmts = ( @@ -3910,6 +3949,7 @@ def test_polygon__invalid_points_values(self): "color": pygame.Color("red"), "points": None, "width": 0, + "border_radius": 1, } points_fmts = ( @@ -3935,6 +3975,7 @@ def test_polygon__valid_color_formats(self): "color": None, "points": ((1, 1), (2, 1), (2, 2), (1, 2)), "width": 0, + "border_radius": 0, } pos = kwargs["points"][0] greens = ( @@ -3965,6 +4006,7 @@ def test_polygon__invalid_color_formats(self): "color": None, "points": ((1, 1), (2, 1), (2, 2), (1, 2)), "width": 0, + "border_radius": 0, } for expected_color in (2.3, self): @@ -3974,7 +4016,7 @@ def test_polygon__invalid_color_formats(self): bounds_rect = self.draw_polygon(**kwargs) def test_draw_square(self): - self.draw_polygon(self.surface, RED, SQUARE, 0) + self.draw_polygon(self.surface, RED, SQUARE, 0, 0) # note : there is a discussion (#234) if draw.polygon should include or # not the right or lower border; here we stick with current behavior, # e.g. include those borders ... @@ -3984,7 +4026,7 @@ def test_draw_square(self): def test_draw_diamond(self): pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) - self.draw_polygon(self.surface, GREEN, DIAMOND, 0) + self.draw_polygon(self.surface, GREEN, DIAMOND, 0, 0) # 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))) @@ -3995,7 +4037,7 @@ def test_draw_diamond(self): 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_polygon(self.surface, GREEN, [(x, 2) for x, _y in CROSS], 0) + self.draw_polygon(self.surface, GREEN, [(x, 2) for x, _y in CROSS], 0, 0) 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) @@ -4003,21 +4045,21 @@ def test_1_pixel_high_or_wide_shapes(self): 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_polygon(self.surface, GREEN, [(x, 5) for x, _y in CROSS], 1) + self.draw_polygon(self.surface, GREEN, [(x, 5) for x, _y in CROSS], 1, 0) 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_polygon(self.surface, GREEN, [(3, y) for _x, y in CROSS], 0) + self.draw_polygon(self.surface, GREEN, [(3, y) for _x, y in CROSS], 0, 0) 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_polygon(self.surface, GREEN, [(4, y) for _x, y in CROSS], 1) + self.draw_polygon(self.surface, GREEN, [(4, y) for _x, y in CROSS], 1, 0) 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) @@ -4030,7 +4072,7 @@ def test_draw_symetric_cross(self): """ # 1. case width = 1 (not filled: `polygon` calls internally the `lines` function) pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) - self.draw_polygon(self.surface, GREEN, CROSS, 1) + self.draw_polygon(self.surface, GREEN, CROSS, 1, 0) 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): @@ -4045,7 +4087,7 @@ def test_draw_symetric_cross(self): # 2. case width = 0 (filled; this is the example from #234) pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) - self.draw_polygon(self.surface, GREEN, CROSS, 0) + self.draw_polygon(self.surface, GREEN, CROSS, 0, 0) 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): @@ -4091,7 +4133,7 @@ def test_illumine_shape(self): pygame.draw.rect(self.surface, RED, (0, 0, 20, 20), 0) # 1. First without the corners 4 & 5 - self.draw_polygon(self.surface, GREEN, path_data[:4], 0) + self.draw_polygon(self.surface, GREEN, path_data[:4], 0, 0) 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): @@ -4099,7 +4141,7 @@ def test_illumine_shape(self): # 2. with the corners 4 & 5 pygame.draw.rect(self.surface, RED, (0, 0, 20, 20), 0) - self.draw_polygon(self.surface, GREEN, path_data, 0) + self.draw_polygon(self.surface, GREEN, path_data, 0, 0) for x in range(4, rect.width - 5 + 1): self.assertEqual(self.surface.get_at((x, 4)), GREEN) # upper inner @@ -4124,6 +4166,7 @@ def test_polygon__bounding_rect(self): sizes = ((min_width, min_height), (max_width, max_height)) surface = pygame.Surface((20, 20), 0, 32) surf_rect = surface.get_rect() + border_radius = 0 # 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) @@ -4151,7 +4194,7 @@ def test_polygon__bounding_rect(self): surface.fill(surf_color) # Clear for each test. bounding_rect = self.draw_polygon( - surface, polygon_color, vertices, thickness + surface, polygon_color, vertices, thickness, border_radius ) # Calculating the expected_rect after the polygon @@ -4176,6 +4219,7 @@ def test_polygon__surface_clip(self): surface_color = pygame.Color("green") surface = pygame.Surface((surfw, surfh)) surface.fill(surface_color) + border_radius = 0 clip_rect = pygame.Rect((0, 0), (8, 10)) clip_rect.center = surface.get_rect().center @@ -4195,7 +4239,7 @@ def test_polygon__surface_clip(self): ) surface.set_clip(None) surface.fill(surface_color) - self.draw_polygon(surface, polygon_color, vertices, width) + self.draw_polygon(surface, polygon_color, vertices, width, border_radius) expected_pts = get_color_points(surface, polygon_color, clip_rect) # Clear the surface and set the clip area. Redraw the polygon @@ -4203,7 +4247,7 @@ def test_polygon__surface_clip(self): surface.fill(surface_color) surface.set_clip(clip_rect) - self.draw_polygon(surface, polygon_color, vertices, width) + self.draw_polygon(surface, polygon_color, vertices, width, border_radius) surface.lock() # For possible speed up. @@ -4226,7 +4270,7 @@ def test_polygon_filled_shape(self): """ key_polygon_points = [(2, 2), (6, 2), (2, 4), (6, 4)] pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) - pygame.draw.polygon(self.surface, GREEN, RHOMBUS, 0) + pygame.draw.polygon(self.surface, GREEN, RHOMBUS, 0, 0) for x, y in key_polygon_points: self.assertEqual(self.surface.get_at((x, y)), GREEN, msg=str((x, y))) From d82da312df1e7a99fb4207326ba2fdecbef8e827 Mon Sep 17 00:00:00 2001 From: anas Date: Tue, 19 Dec 2023 22:35:11 +0100 Subject: [PATCH 12/17] Fixed format --- test/draw_test.py | 50 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/test/draw_test.py b/test/draw_test.py index d8572debea..38a333df2c 100644 --- a/test/draw_test.py +++ b/test/draw_test.py @@ -3657,7 +3657,7 @@ def test_polygon__args_without_radius(self): ) self.assertIsInstance(bounds_rect, pygame.Rect) - + def test_polygon__args_without_width_nor_radius(self): """Ensures draw polygon accepts the args without a width.""" bounds_rect = self.draw_polygon( @@ -3674,7 +3674,13 @@ def test_polygon__kwargs(self): color = pygame.Color("yellow") points = ((0, 0), (1, 1), (2, 2)) kwargs_list = [ - {"surface": surface, "color": color, "points": points, "width": 1, "border_radius": 1}, + { + "surface": surface, + "color": color, + "points": points, + "width": 1, + "border_radius": 1, + }, {"surface": surface, "color": color, "points": points, "width": 1}, {"surface": surface, "color": color, "points": points}, ] @@ -3736,7 +3742,7 @@ def test_polygon__arg_invalid_types(self): with self.assertRaises(TypeError): # Invalid border_radius. bounds_rect = self.draw_polygon(surface, color, points, 1, "1") - + with self.assertRaises(TypeError): # Invalid width. bounds_rect = self.draw_polygon(surface, color, points, "1") @@ -3770,7 +3776,7 @@ def test_polygon__kwarg_invalid_types(self): }, { "surface": surface, - "color": 2.3, # Invalid color. + "color": 2.3, # Invalid color. "points": points, "width": width, "border_radius": border_radius, @@ -3786,7 +3792,7 @@ def test_polygon__kwarg_invalid_types(self): "surface": surface, "color": color, "points": points, - "width": 1.2, # Invalid width. + "width": 1.2, # Invalid width. "border_radius": border_radius, }, { @@ -3794,9 +3800,9 @@ def test_polygon__kwarg_invalid_types(self): "color": color, "points": points, "width": width, - "border_radius": 2.3, # Invalid border_radius. + "border_radius": 2.3, # Invalid border_radius. }, - ] + ] for kwargs in kwargs_list: with self.assertRaises(TypeError): @@ -3816,7 +3822,13 @@ def test_polygon__kwarg_invalid_name(self): "border_radius": 1, "invalid": 1, }, - {"surface": surface, "color": color, "points": points, "invalid": 1, "border_radius": 1}, + { + "surface": surface, + "color": color, + "points": points, + "invalid": 1, + "border_radius": 1, + }, ] for kwargs in kwargs_list: @@ -3830,7 +3842,13 @@ def test_polygon__args_and_kwargs(self): points = ((0, 1), (1, 2), (2, 3)) width = 0 border_radius = 0 - kwargs = {"surface": surface, "color": color, "points": points, "width": width, "border_radius": border_radius} + kwargs = { + "surface": surface, + "color": color, + "points": points, + "width": width, + "border_radius": border_radius, + } for name in ("surface", "color", "points", "width", "border_radius"): kwargs.pop(name) @@ -3844,10 +3862,12 @@ def test_polygon__args_and_kwargs(self): elif "width" == name: bounds_rect = self.draw_polygon(surface, color, points, width, **kwargs) else: - bounds_rect = self.draw_polygon(surface, color, points, width, border_radius, **kwargs) + bounds_rect = self.draw_polygon( + surface, color, points, width, border_radius, **kwargs + ) self.assertIsInstance(bounds_rect, pygame.Rect) - + def test_polygon__valid_width_values(self): """Ensures draw polygon accepts different width values.""" surface_color = pygame.Color("white") @@ -4239,7 +4259,9 @@ def test_polygon__surface_clip(self): ) surface.set_clip(None) surface.fill(surface_color) - self.draw_polygon(surface, polygon_color, vertices, width, border_radius) + self.draw_polygon( + surface, polygon_color, vertices, width, border_radius + ) expected_pts = get_color_points(surface, polygon_color, clip_rect) # Clear the surface and set the clip area. Redraw the polygon @@ -4247,7 +4269,9 @@ def test_polygon__surface_clip(self): surface.fill(surface_color) surface.set_clip(clip_rect) - self.draw_polygon(surface, polygon_color, vertices, width, border_radius) + self.draw_polygon( + surface, polygon_color, vertices, width, border_radius + ) surface.lock() # For possible speed up. From 7e7a525b4ad2fc3f65add4dabb96df61d9175759 Mon Sep 17 00:00:00 2001 From: anas Date: Wed, 20 Dec 2023 16:25:24 +0100 Subject: [PATCH 13/17] Added comments, changed function name and modified condition --- src_c/draw.c | 62 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/src_c/draw.c b/src_c/draw.c index 56bce84abe..ed3157b5ae 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -730,8 +730,8 @@ polygon(PyObject *self, PyObject *arg, PyObject *kwargs) 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", - "width", "border_radius", NULL}; + static char *keywords[] = {"surface", "color", "points", + "width", "border_radius", NULL}; if (!PyArg_ParseTupleAndKeywords(arg, kwargs, "O!OO|ii", keywords, &pgSurface_Type, &surfobj, &colorobj, @@ -2874,9 +2874,9 @@ side(Point a, Point b, Point c) } // Function to find a parallel line to the line formed by 'pt1' and 'pt2' at a -// given distance 'pt3' is used to determin in which side of the line, the -// parallel line should be drawn Returns a Line struct representing the -// parallel line +// given distance. 'pt3' is used to determine in which side of the line, the +// parallel line should be drawn. +// Returns a Line struct representing the parallel line Line find_parallel_line(Point pt1, Point pt2, Point pt3, int distance) { @@ -2954,17 +2954,21 @@ project_point_onto_segment(Point point, Point segment_start, Point segment_end) return projection; } -// Function to find the intersection point of two line segments -// Returns the intersection point as a Point struct +// Function to find the intersection point of two line segments. +// Returns the intersection point as a Point struct. Point intersection(Point line1_start, Point line1_end, Point line2_start, Point line2_end) { - // Calculate coefficients for each line equation + // Calculate line equation coefficients where the form of the equation is : + // AX + BY = C + + // Coefficients for the equation of the first line (A1*X + B1*Y = C1) double A1 = line1_end.y - line1_start.y; double B1 = line1_start.x - line1_end.x; double C1 = A1 * line1_start.x + B1 * line1_start.y; + // Coefficients for the equation of the second line (A2*X + B2*Y = C2) double A2 = line2_end.y - line2_start.y; double B2 = line2_start.x - line2_end.x; double C2 = A2 * line2_start.x + B2 * line2_start.y; @@ -2987,9 +2991,9 @@ intersection(Point line1_start, Point line1_end, Point line2_start, } // Function to calculate the angle (in radians) between a center point and -// another point +// another point. double -angle(Point center, Point point) +computeVectorAngle(Point center, Point point) { // Calculate the differences in x and y coordinates between the two points double x = point.x - center.x; @@ -3043,7 +3047,7 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, int b = (i + 1) % (num_points); // index of the point after index i // Check if the border-radius can be applied to the current angle - // it can not be applied to a flat angle(if the three points are + // it can not be applied to a flat angle (if the three points are // aligned) if (side(points[a], points[i], points[b]) == 0) { PyErr_SetString( @@ -3051,6 +3055,9 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, "Border-radius cannot be applied to a flat angle.\nPlease " "ensure that the specified angle or curvature is within a " "valid range for border-radius drawing.\n"); + free(path); + free(circles); + free(points); return -1; } @@ -3067,19 +3074,28 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, Point proj2 = project_point_onto_segment(circles[i].center, points[i], points[b]); + // distance from the vertex to the projetion of the circle center on + // the first edge + double distanceProj1 = sqrt(pow(proj1.x - points[i].x, 2) + + pow(proj1.y - points[i].y, 2)); + // distance from the vertex to the projetion of the circle center on + // the second edge + double distanceProj2 = sqrt(pow(proj2.x - points[i].x, 2) + + pow(proj2.y - points[i].y, 2)); + + // lenghts of both edges + double baseLength1 = sqrt(pow(points[a].x - points[i].x, 2) + + pow(points[a].y - points[i].y, 2)); + double baseLength2 = sqrt(pow(points[b].x - points[i].x, 2) + + pow(points[b].y - points[i].y, 2)); + // Check if the border-radius size is valid - if ((sqrt(pow(proj1.x - points[i].x, 2) + - pow(proj1.y - points[i].y, 2)) >= - sqrt(pow(points[a].x - points[i].x, 2) + - pow(points[a].y - points[i].y, 2)) / - 2) || - (sqrt(pow(proj2.x - points[i].x, 2) + - pow(proj2.y - points[i].y, 2)) >= - sqrt(pow(points[b].x - points[i].x, 2) + - pow(points[b].y - points[i].y, 2)) / - 2)) { + if ((distanceProj1 >= baseLength1 / 2) || (distanceProj2 >= baseLength2 / 2)) { PyErr_SetString(PyExc_ValueError, "Border-radius size must be smaller\n"); + free(path); + free(circles); + free(points); return -1; } @@ -3097,8 +3113,8 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, Point pt3 = path[(i + 2) % (2 * num_points)]; Circle circle = circles[i / 2]; - double start_angle = angle(circle.center, pt1); - double end_angle = angle(circle.center, pt2); + double start_angle = computeVectorAngle(circle.center, pt1); + double end_angle = computeVectorAngle(circle.center, pt2); // Adjust angles based on the side of the polygon if (side(pt1, pt2, pt3) == 1) { From f2bf113963cfa89e0cbb0f220e72275bf973be90 Mon Sep 17 00:00:00 2001 From: anas Date: Thu, 21 Dec 2023 23:57:04 +0100 Subject: [PATCH 14/17] Refactored intersection and find_parallel_line functions to improve error handling and return types. --- src_c/draw.c | 100 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 32 deletions(-) diff --git a/src_c/draw.c b/src_c/draw.c index ed3157b5ae..e346f696dd 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -730,8 +730,8 @@ polygon(PyObject *self, PyObject *arg, PyObject *kwargs) 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", - "width", "border_radius", NULL}; + static char *keywords[] = {"surface", "color", "points", + "width", "border_radius", NULL}; if (!PyArg_ParseTupleAndKeywords(arg, kwargs, "O!OO|ii", keywords, &pgSurface_Type, &surfobj, &colorobj, @@ -2877,8 +2877,9 @@ side(Point a, Point b, Point c) // given distance. 'pt3' is used to determine in which side of the line, the // parallel line should be drawn. // Returns a Line struct representing the parallel line -Line -find_parallel_line(Point pt1, Point pt2, Point pt3, int distance) +int +find_parallel_line(Point pt1, Point pt2, Point pt3, int distance, + Line *line_result) { // Calculate direction vector, normalize it, and find the perpendicular // vector @@ -2890,6 +2891,10 @@ find_parallel_line(Point pt1, Point pt2, Point pt3, int distance) double magnitude = sqrt(direction_vector.x * direction_vector.x + direction_vector.y * direction_vector.y); + if (magnitude == 0) { + return 0; + } + // Normalize the direction vector to get a unit vector Point normalized_direction = {direction_vector.x / magnitude, direction_vector.y / magnitude}; @@ -2910,11 +2915,13 @@ find_parallel_line(Point pt1, Point pt2, Point pt3, int distance) perpendicular_vector.y * distance}; // Generate the points for the parallel line based on the offset - Line parallel_line = {{pt1.x + offset_vector.x, pt1.y + offset_vector.y}, - {pt2.x + offset_vector.x, pt2.y + offset_vector.y}}; + line_result->start = + (Point){pt1.x + offset_vector.x, pt1.y + offset_vector.y}; + line_result->end = + (Point){pt2.x + offset_vector.x, pt2.y + offset_vector.y}; // Return the Line struct representing the parallel line - return parallel_line; + return 1; } // Function to project a point onto a line segment defined by 'segment_start' @@ -2942,7 +2949,7 @@ project_point_onto_segment(Point point, Point segment_start, Point segment_end) // Ensure 't' is within the valid range [0, 1] to make sure that the // projection of the point onto the line segment lies specifically within - // the boundaries of the segment See line 2906 + // the boundaries of the segment. (See line 2906) t = fmax(0, fmin(1, t)); // Calculate the coordinates of the projected point using the parameter 't' @@ -2956,9 +2963,9 @@ project_point_onto_segment(Point point, Point segment_start, Point segment_end) // Function to find the intersection point of two line segments. // Returns the intersection point as a Point struct. -Point -intersection(Point line1_start, Point line1_end, Point line2_start, - Point line2_end) +int +calculate_intersection(Point line1_start, Point line1_end, Point line2_start, + Point line2_end, Point *intersection_result) { // Calculate line equation coefficients where the form of the equation is : // AX + BY = C @@ -2978,15 +2985,18 @@ intersection(Point line1_start, Point line1_end, Point line2_start, // If the lines are parallel (det == 0), they do not intersect, return a // default Point if (det == 0) { - return (Point){0, 0}; + return 0; } else { - // Calculate the intersection point using Cramer's rule + // Calculate the intersection point using Cramer's rule. // The lines never intersect out of the range given because of the - // condition limiting the value of boarder radius + // condition limiting the value of boarder radius. double x = (B2 * C1 - B1 * C2) / det; double y = (A1 * C2 - A2 * C1) / det; - return (Point){x, y}; + + intersection_result->x = x; + intersection_result->y = y; + return 1; } } @@ -2999,8 +3009,8 @@ computeVectorAngle(Point center, Point point) double x = point.x - center.x; double y = point.y - center.y; // Use atan2 to calculate the angle formed by the vector from the center to - // the point The angle is measured counterclockwise from the positive - // x-axis Note: The negative sign is used to ensure the angle is measured + // the point. The angle is measured counterclockwise from the positive + // x-axis. Note: The negative sign is used to ensure the angle is measured // clockwise, which is more common in Cartesian coordinates. return -atan2(y, x); } @@ -3011,6 +3021,10 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, int border_radius, int num_points, Uint32 color, int *drawn_area) { + Point intersection; + Line line1; + Line line2; + // Define arrays to store path points and circle information Point *path; Circle *circles; @@ -3054,7 +3068,7 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, PyExc_ValueError, "Border-radius cannot be applied to a flat angle.\nPlease " "ensure that the specified angle or curvature is within a " - "valid range for border-radius drawing.\n"); + "valid range for border-radius drawing."); free(path); free(circles); free(points); @@ -3062,12 +3076,33 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, } // Find parallel lines to the polygon sides at the current point - Line line1 = - find_parallel_line(points[a], points[i], points[b], border_radius); - Line line2 = - find_parallel_line(points[i], points[b], points[a], border_radius); - circles[i].center = - intersection(line1.start, line1.end, line2.start, line2.end); + if (!find_parallel_line(points[a], points[i], points[b], border_radius, + &line1) || + !find_parallel_line(points[i], points[b], points[a], border_radius, + &line2)) { + PyErr_SetString( + PyExc_ValueError, + "Points parameter must consist of sequentially points, and no " + "three consecutive points should align."); + free(path); + free(circles); + free(points); + return -1; + } + + if (!calculate_intersection(line1.start, line1.end, line2.start, + line2.end, &intersection)) { + PyErr_SetString( + PyExc_ValueError, + "Points parameter must consist of sequentially points, and no " + "three consecutive points should align."); + free(path); + free(circles); + free(points); + return -1; + } + + circles[i].center = intersection; Point proj1 = project_point_onto_segment(circles[i].center, points[a], points[i]); @@ -3090,9 +3125,10 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, pow(points[b].y - points[i].y, 2)); // Check if the border-radius size is valid - if ((distanceProj1 >= baseLength1 / 2) || (distanceProj2 >= baseLength2 / 2)) { + if ((distanceProj1 >= baseLength1 / 2) || + (distanceProj2 >= baseLength2 / 2)) { PyErr_SetString(PyExc_ValueError, - "Border-radius size must be smaller\n"); + "Border-radius size must be smaller"); free(path); free(circles); free(points); @@ -3123,12 +3159,12 @@ draw_round_polygon(SDL_Surface *surf, int *pts_x, int *pts_y, int width, end_angle = temp; } - int circle_center_x = round(circle.center.x); - int circle_center_y = round(circle.center.y); - int pt2_x = round(pt2.x); - int pt2_y = round(pt2.y); - int pt3_x = round(pt3.x); - int pt3_y = round(pt3.y); + int circle_center_x = (int)round(circle.center.x); + int circle_center_y = (int)round(circle.center.y); + int pt2_x = (int)round(pt2.x); + int pt2_y = (int)round(pt2.y); + int pt3_x = (int)round(pt3.x); + int pt3_y = (int)round(pt3.y); draw_arc(surf, circle_center_x, circle_center_y, circle.radius, circle.radius, width, start_angle, end_angle, color, From 47ae925f33ad432cdf90f61ca56fce6ea4f255e7 Mon Sep 17 00:00:00 2001 From: anas Date: Sun, 24 Dec 2023 21:00:12 +0100 Subject: [PATCH 15/17] Added tests --- test/draw_test.py | 236 ++++++++++++++++++++++++++++------------------ 1 file changed, 146 insertions(+), 90 deletions(-) diff --git a/test/draw_test.py b/test/draw_test.py index 38a333df2c..2d41dee297 100644 --- a/test/draw_test.py +++ b/test/draw_test.py @@ -3645,20 +3645,12 @@ def setUp(self): def test_polygon__args(self): """Ensures draw polygon accepts the correct args.""" bounds_rect = self.draw_polygon( - pygame.Surface((3, 3)), (0, 10, 0, 50), ((0, 0), (1, 1), (2, 2)), 1, 1 + pygame.Surface((3, 3)), (0, 10, 0, 50), ((0, 0), (1, 1), (2, 2)), 1 ) self.assertIsInstance(bounds_rect, pygame.Rect) - def test_polygon__args_without_radius(self): - """Ensures draw polygon accepts the args without a width.""" - bounds_rect = self.draw_polygon( - pygame.Surface((2, 2)), (0, 0, 0, 50), ((0, 0), (1, 1), (2, 2)), 1 - ) - - self.assertIsInstance(bounds_rect, pygame.Rect) - - def test_polygon__args_without_width_nor_radius(self): + def test_polygon__args_without_width(self): """Ensures draw polygon accepts the args without a width.""" bounds_rect = self.draw_polygon( pygame.Surface((2, 2)), (0, 0, 0, 50), ((0, 0), (1, 1), (2, 2)) @@ -3674,13 +3666,6 @@ def test_polygon__kwargs(self): color = pygame.Color("yellow") points = ((0, 0), (1, 1), (2, 2)) kwargs_list = [ - { - "surface": surface, - "color": color, - "points": points, - "width": 1, - "border_radius": 1, - }, {"surface": surface, "color": color, "points": points, "width": 1}, {"surface": surface, "color": color, "points": points}, ] @@ -3695,7 +3680,6 @@ def test_polygon__kwargs_order_independent(self): bounds_rect = self.draw_polygon( color=(10, 20, 30), surface=pygame.Surface((3, 2)), - border_radius=1, width=0, points=((0, 1), (1, 2), (2, 3)), ) @@ -3723,7 +3707,6 @@ def test_polygon__kwargs_missing(self): "color": pygame.Color("red"), "points": ((2, 1), (2, 2), (2, 3)), "width": 1, - "border_radius": 1, } for name in ("points", "color", "surface"): @@ -3739,10 +3722,6 @@ def test_polygon__arg_invalid_types(self): color = pygame.Color("blue") points = ((0, 1), (1, 2), (1, 3)) - with self.assertRaises(TypeError): - # Invalid border_radius. - bounds_rect = self.draw_polygon(surface, color, points, 1, "1") - with self.assertRaises(TypeError): # Invalid width. bounds_rect = self.draw_polygon(surface, color, points, "1") @@ -3765,44 +3744,27 @@ def test_polygon__kwarg_invalid_types(self): color = pygame.Color("green") points = ((0, 0), (1, 0), (2, 0)) width = 1 - border_radius = 1 kwargs_list = [ { "surface": pygame.Surface, # Invalid surface. "color": color, "points": points, "width": width, - "border_radius": border_radius, }, { "surface": surface, "color": 2.3, # Invalid color. "points": points, "width": width, - "border_radius": border_radius, }, { "surface": surface, "color": color, "points": ((1,), (1,), (1,)), # Invalid points. "width": width, - "border_radius": border_radius, - }, - { - "surface": surface, - "color": color, - "points": points, - "width": 1.2, # Invalid width. - "border_radius": border_radius, - }, - { - "surface": surface, - "color": color, - "points": points, - "width": width, - "border_radius": 2.3, # Invalid border_radius. }, - ] + {"surface": surface, "color": color, "points": points, "width": 1.2}, + ] # Invalid width. for kwargs in kwargs_list: with self.assertRaises(TypeError): @@ -3819,16 +3781,9 @@ def test_polygon__kwarg_invalid_name(self): "color": color, "points": points, "width": 1, - "border_radius": 1, - "invalid": 1, - }, - { - "surface": surface, - "color": color, - "points": points, "invalid": 1, - "border_radius": 1, }, + {"surface": surface, "color": color, "points": points, "invalid": 1}, ] for kwargs in kwargs_list: @@ -3841,16 +3796,9 @@ def test_polygon__args_and_kwargs(self): color = (255, 255, 0, 0) points = ((0, 1), (1, 2), (2, 3)) width = 0 - border_radius = 0 - kwargs = { - "surface": surface, - "color": color, - "points": points, - "width": width, - "border_radius": border_radius, - } + kwargs = {"surface": surface, "color": color, "points": points, "width": width} - for name in ("surface", "color", "points", "width", "border_radius"): + for name in ("surface", "color", "points", "width"): kwargs.pop(name) if "surface" == name: @@ -3859,12 +3807,8 @@ def test_polygon__args_and_kwargs(self): bounds_rect = self.draw_polygon(surface, color, **kwargs) elif "points" == name: bounds_rect = self.draw_polygon(surface, color, points, **kwargs) - elif "width" == name: - bounds_rect = self.draw_polygon(surface, color, points, width, **kwargs) else: - bounds_rect = self.draw_polygon( - surface, color, points, width, border_radius, **kwargs - ) + bounds_rect = self.draw_polygon(surface, color, points, width, **kwargs) self.assertIsInstance(bounds_rect, pygame.Rect) @@ -3878,7 +3822,6 @@ def test_polygon__valid_width_values(self): "color": color, "points": ((1, 1), (2, 1), (2, 2), (1, 2)), "width": None, - "border_radius": 0, } pos = kwargs["points"][0] @@ -3902,7 +3845,6 @@ def test_polygon__valid_points_format(self): "color": expected_color, "points": None, "width": 0, - "border_radius": 0, } # The point type can be a tuple/list/Vector2. @@ -3943,7 +3885,6 @@ def test_polygon__invalid_points_formats(self): "color": pygame.Color("red"), "points": None, "width": 0, - "border_radius": 0, } points_fmts = ( @@ -3969,7 +3910,6 @@ def test_polygon__invalid_points_values(self): "color": pygame.Color("red"), "points": None, "width": 0, - "border_radius": 1, } points_fmts = ( @@ -3995,7 +3935,6 @@ def test_polygon__valid_color_formats(self): "color": None, "points": ((1, 1), (2, 1), (2, 2), (1, 2)), "width": 0, - "border_radius": 0, } pos = kwargs["points"][0] greens = ( @@ -4026,7 +3965,6 @@ def test_polygon__invalid_color_formats(self): "color": None, "points": ((1, 1), (2, 1), (2, 2), (1, 2)), "width": 0, - "border_radius": 0, } for expected_color in (2.3, self): @@ -4036,7 +3974,7 @@ def test_polygon__invalid_color_formats(self): bounds_rect = self.draw_polygon(**kwargs) def test_draw_square(self): - self.draw_polygon(self.surface, RED, SQUARE, 0, 0) + self.draw_polygon(self.surface, RED, SQUARE, 0) # note : there is a discussion (#234) if draw.polygon should include or # not the right or lower border; here we stick with current behavior, # e.g. include those borders ... @@ -4046,7 +3984,7 @@ def test_draw_square(self): def test_draw_diamond(self): pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) - self.draw_polygon(self.surface, GREEN, DIAMOND, 0, 0) + self.draw_polygon(self.surface, GREEN, DIAMOND, 0) # 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))) @@ -4057,7 +3995,7 @@ def test_draw_diamond(self): 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_polygon(self.surface, GREEN, [(x, 2) for x, _y in CROSS], 0, 0) + self.draw_polygon(self.surface, GREEN, [(x, 2) for x, _y in CROSS], 0) 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) @@ -4065,21 +4003,21 @@ def test_1_pixel_high_or_wide_shapes(self): 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_polygon(self.surface, GREEN, [(x, 5) for x, _y in CROSS], 1, 0) + self.draw_polygon(self.surface, GREEN, [(x, 5) for x, _y in CROSS], 1) 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_polygon(self.surface, GREEN, [(3, y) for _x, y in CROSS], 0, 0) + self.draw_polygon(self.surface, GREEN, [(3, y) for _x, y in CROSS], 0) 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_polygon(self.surface, GREEN, [(4, y) for _x, y in CROSS], 1, 0) + self.draw_polygon(self.surface, GREEN, [(4, y) for _x, y in CROSS], 1) 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) @@ -4092,7 +4030,7 @@ def test_draw_symetric_cross(self): """ # 1. case width = 1 (not filled: `polygon` calls internally the `lines` function) pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) - self.draw_polygon(self.surface, GREEN, CROSS, 1, 0) + self.draw_polygon(self.surface, GREEN, CROSS, 1) 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): @@ -4107,7 +4045,7 @@ def test_draw_symetric_cross(self): # 2. case width = 0 (filled; this is the example from #234) pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) - self.draw_polygon(self.surface, GREEN, CROSS, 0, 0) + self.draw_polygon(self.surface, GREEN, CROSS, 0) 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): @@ -4153,7 +4091,7 @@ def test_illumine_shape(self): pygame.draw.rect(self.surface, RED, (0, 0, 20, 20), 0) # 1. First without the corners 4 & 5 - self.draw_polygon(self.surface, GREEN, path_data[:4], 0, 0) + self.draw_polygon(self.surface, GREEN, path_data[:4], 0) 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): @@ -4161,7 +4099,7 @@ def test_illumine_shape(self): # 2. with the corners 4 & 5 pygame.draw.rect(self.surface, RED, (0, 0, 20, 20), 0) - self.draw_polygon(self.surface, GREEN, path_data, 0, 0) + self.draw_polygon(self.surface, GREEN, path_data, 0) for x in range(4, rect.width - 5 + 1): self.assertEqual(self.surface.get_at((x, 4)), GREEN) # upper inner @@ -4186,7 +4124,6 @@ def test_polygon__bounding_rect(self): sizes = ((min_width, min_height), (max_width, max_height)) surface = pygame.Surface((20, 20), 0, 32) surf_rect = surface.get_rect() - border_radius = 0 # 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) @@ -4214,7 +4151,7 @@ def test_polygon__bounding_rect(self): surface.fill(surf_color) # Clear for each test. bounding_rect = self.draw_polygon( - surface, polygon_color, vertices, thickness, border_radius + surface, polygon_color, vertices, thickness ) # Calculating the expected_rect after the polygon @@ -4239,7 +4176,6 @@ def test_polygon__surface_clip(self): surface_color = pygame.Color("green") surface = pygame.Surface((surfw, surfh)) surface.fill(surface_color) - border_radius = 0 clip_rect = pygame.Rect((0, 0), (8, 10)) clip_rect.center = surface.get_rect().center @@ -4259,9 +4195,7 @@ def test_polygon__surface_clip(self): ) surface.set_clip(None) surface.fill(surface_color) - self.draw_polygon( - surface, polygon_color, vertices, width, border_radius - ) + self.draw_polygon(surface, polygon_color, vertices, width) expected_pts = get_color_points(surface, polygon_color, clip_rect) # Clear the surface and set the clip area. Redraw the polygon @@ -4269,9 +4203,7 @@ def test_polygon__surface_clip(self): surface.fill(surface_color) surface.set_clip(clip_rect) - self.draw_polygon( - surface, polygon_color, vertices, width, border_radius - ) + self.draw_polygon(surface, polygon_color, vertices, width) surface.lock() # For possible speed up. @@ -4294,9 +4226,133 @@ def test_polygon_filled_shape(self): """ key_polygon_points = [(2, 2), (6, 2), (2, 4), (6, 4)] pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) - pygame.draw.polygon(self.surface, GREEN, RHOMBUS, 0, 0) + pygame.draw.polygon(self.surface, GREEN, RHOMBUS, 0) for x, y in key_polygon_points: self.assertEqual(self.surface.get_at((x, y)), GREEN, msg=str((x, y))) + + def test_polygon__border_radius_args(self): + """Ensures draw polygon accepts border_radius args.""" + bounds_rect = self.draw_polygon( + pygame.Surface((400, 400)), (0, 10, 0, 50), [[100,100], [300, 100], [300, 300], [100, 300]], 1, 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_polygon__border_radius_kwarg(self): + """Ensures draw polygon accepts border_radius kwarg + with and without a width arg and with different orders. + """ + surface = pygame.Surface((400, 400)) + color = pygame.Color("yellow") + points = [[100,100], [300, 100], [300, 300], [100, 300]] + kwargs_list = [ + {"surface": surface, "color": color, "points": points, "width": 1, "border_radius": 1}, + {"surface": surface, "color": color, "points": points, "border_radius": 1}, + {"points": points, "surface": surface, "border_radius": 1, "color": color, "width": 1}, + {"surface": surface, "color": color, "border_radius": 1, "points": points}, + {"border_radius": 1, "surface": surface, "color": color, "points": points}, + ] + + for kwargs in kwargs_list: + bounds_rect = self.draw_polygon(**kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_polygon__arg_border_radius_invalid_type(self): + """Ensures draw polygon detects invalid border_radius arg type.""" + surface = pygame.Surface((400, 400)) + color = pygame.Color("blue") + points = [[100,100], [300, 100], [300, 300], [100, 300]] + + with self.assertRaises(TypeError): + # Invalid radius. + bounds_rect = self.draw_polygon(surface, color, points, 1, "1") + + def test_polygon__kwarg_border_radius_invalid_type_name(self): + """Ensures draw polygon detects invalid kwarg border_radius type and name.""" + surface = pygame.Surface((400, 400)) + color = pygame.Color("green") + points = [[100,100], [300, 100], [300, 300], [100, 300]] + width = 1 + kwargs_list = [ + { + "surface": surface, + "color": color, + "points": points, + "width": width, + "border_radius": "1" # Invalid border_radius type + }, + { + "surface": surface, + "color": color, + "points": points, + "width": width, + "border_radius": 2.3 # Invalid border_radius type + }, + { + "surface": surface, + "color": color, + "points": points, + "width": width, + "Invalid": 1 # Invalid border_radius name + }, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_polygon(**kwargs) + + def test_polygon__args_and_kwargs_border_radius(self): + """Ensures draw polygon accepts a combination of args/kwargs with border_radius""" + surface = pygame.Surface((400, 400)) + color = (255, 255, 0, 0) + points = [[100,100], [300, 100], [300, 300], [100, 300]] + width = 1 + border_radius = 1 + kwargs = {"surface": surface, "color": color, "points": points, "width": width, "border_radius": border_radius} + + for name in ("surface", "color", "points", "width", "border_radius"): + kwargs.pop(name) + + if "surface" == name: + bounds_rect = self.draw_polygon(surface, **kwargs) + elif "color" == name: + bounds_rect = self.draw_polygon(surface, color, **kwargs) + elif "points" == name: + bounds_rect = self.draw_polygon(surface, color, points, **kwargs) + elif "width" == name: + bounds_rect = self.draw_polygon(surface, color, points, width, **kwargs) + else: + bounds_rect = self.draw_polygon(surface, color, points, width, border_radius, **kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_polygon__valid_border_radius_values(self): + """Ensures draw polygon accepts different border_radius values.""" + surface_color = pygame.Color("white") + surface = pygame.Surface((400, 400)) + color = (10, 20, 30, 255) + kwargs = { + "surface": surface, + "color": color, + "points": [[100,100], [300, 100], [300, 300], [100, 300]], + "width": 1, + "border_radius": None, + } + + pos1 = kwargs["points"][0] + pos2 = kwargs["points"][1] + pos = ((pos1[0] + pos2[0]) / 2, (pos1[1] + pos2[1]) / 2) + + for border_radius in (0, 1, 10, 50): + surface.fill(surface_color) + kwargs["border_radius"] = border_radius + expected_color = color + + bounds_rect = self.draw_polygon(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) class DrawPolygonTest(DrawPolygonMixin, DrawTestCase): From 277a00dc72649f4864cb0557748318977e5ff2c0 Mon Sep 17 00:00:00 2001 From: anas Date: Sun, 24 Dec 2023 21:07:35 +0100 Subject: [PATCH 16/17] Fixed format --- test/draw_test.py | 62 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/test/draw_test.py b/test/draw_test.py index 2d41dee297..3aadeb4ccc 100644 --- a/test/draw_test.py +++ b/test/draw_test.py @@ -4229,26 +4229,42 @@ def test_polygon_filled_shape(self): pygame.draw.polygon(self.surface, GREEN, RHOMBUS, 0) for x, y in key_polygon_points: self.assertEqual(self.surface.get_at((x, y)), GREEN, msg=str((x, y))) - + def test_polygon__border_radius_args(self): """Ensures draw polygon accepts border_radius args.""" bounds_rect = self.draw_polygon( - pygame.Surface((400, 400)), (0, 10, 0, 50), [[100,100], [300, 100], [300, 300], [100, 300]], 1, 1 + pygame.Surface((400, 400)), + (0, 10, 0, 50), + [[100, 100], [300, 100], [300, 300], [100, 300]], + 1, + 1, ) self.assertIsInstance(bounds_rect, pygame.Rect) - + def test_polygon__border_radius_kwarg(self): """Ensures draw polygon accepts border_radius kwarg with and without a width arg and with different orders. """ surface = pygame.Surface((400, 400)) color = pygame.Color("yellow") - points = [[100,100], [300, 100], [300, 300], [100, 300]] + points = [[100, 100], [300, 100], [300, 300], [100, 300]] kwargs_list = [ - {"surface": surface, "color": color, "points": points, "width": 1, "border_radius": 1}, + { + "surface": surface, + "color": color, + "points": points, + "width": 1, + "border_radius": 1, + }, {"surface": surface, "color": color, "points": points, "border_radius": 1}, - {"points": points, "surface": surface, "border_radius": 1, "color": color, "width": 1}, + { + "points": points, + "surface": surface, + "border_radius": 1, + "color": color, + "width": 1, + }, {"surface": surface, "color": color, "border_radius": 1, "points": points}, {"border_radius": 1, "surface": surface, "color": color, "points": points}, ] @@ -4262,17 +4278,17 @@ def test_polygon__arg_border_radius_invalid_type(self): """Ensures draw polygon detects invalid border_radius arg type.""" surface = pygame.Surface((400, 400)) color = pygame.Color("blue") - points = [[100,100], [300, 100], [300, 300], [100, 300]] + points = [[100, 100], [300, 100], [300, 300], [100, 300]] with self.assertRaises(TypeError): # Invalid radius. bounds_rect = self.draw_polygon(surface, color, points, 1, "1") - + def test_polygon__kwarg_border_radius_invalid_type_name(self): """Ensures draw polygon detects invalid kwarg border_radius type and name.""" surface = pygame.Surface((400, 400)) color = pygame.Color("green") - points = [[100,100], [300, 100], [300, 300], [100, 300]] + points = [[100, 100], [300, 100], [300, 300], [100, 300]] width = 1 kwargs_list = [ { @@ -4280,36 +4296,42 @@ def test_polygon__kwarg_border_radius_invalid_type_name(self): "color": color, "points": points, "width": width, - "border_radius": "1" # Invalid border_radius type + "border_radius": "1", # Invalid border_radius type }, { "surface": surface, "color": color, "points": points, "width": width, - "border_radius": 2.3 # Invalid border_radius type + "border_radius": 2.3, # Invalid border_radius type }, { "surface": surface, "color": color, "points": points, "width": width, - "Invalid": 1 # Invalid border_radius name + "Invalid": 1, # Invalid border_radius name }, ] for kwargs in kwargs_list: with self.assertRaises(TypeError): bounds_rect = self.draw_polygon(**kwargs) - + def test_polygon__args_and_kwargs_border_radius(self): """Ensures draw polygon accepts a combination of args/kwargs with border_radius""" surface = pygame.Surface((400, 400)) color = (255, 255, 0, 0) - points = [[100,100], [300, 100], [300, 300], [100, 300]] + points = [[100, 100], [300, 100], [300, 300], [100, 300]] width = 1 border_radius = 1 - kwargs = {"surface": surface, "color": color, "points": points, "width": width, "border_radius": border_radius} + kwargs = { + "surface": surface, + "color": color, + "points": points, + "width": width, + "border_radius": border_radius, + } for name in ("surface", "color", "points", "width", "border_radius"): kwargs.pop(name) @@ -4323,10 +4345,12 @@ def test_polygon__args_and_kwargs_border_radius(self): elif "width" == name: bounds_rect = self.draw_polygon(surface, color, points, width, **kwargs) else: - bounds_rect = self.draw_polygon(surface, color, points, width, border_radius, **kwargs) + bounds_rect = self.draw_polygon( + surface, color, points, width, border_radius, **kwargs + ) self.assertIsInstance(bounds_rect, pygame.Rect) - + def test_polygon__valid_border_radius_values(self): """Ensures draw polygon accepts different border_radius values.""" surface_color = pygame.Color("white") @@ -4335,11 +4359,11 @@ def test_polygon__valid_border_radius_values(self): kwargs = { "surface": surface, "color": color, - "points": [[100,100], [300, 100], [300, 300], [100, 300]], + "points": [[100, 100], [300, 100], [300, 300], [100, 300]], "width": 1, "border_radius": None, } - + pos1 = kwargs["points"][0] pos2 = kwargs["points"][1] pos = ((pos1[0] + pos2[0]) / 2, (pos1[1] + pos2[1]) / 2) From 65dd610c212a6ae0b15f483a4883fa2b639256db Mon Sep 17 00:00:00 2001 From: anas Date: Wed, 27 Dec 2023 15:21:06 +0100 Subject: [PATCH 17/17] Added test for drawing rounded square, diamond, rhombus and cross --- test/draw_test.py | 116 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/test/draw_test.py b/test/draw_test.py index 3aadeb4ccc..0463482f38 100644 --- a/test/draw_test.py +++ b/test/draw_test.py @@ -3632,6 +3632,25 @@ class to add any draw.aalines specific tests to. [2, 2], ) +# Enlarged versions of the same shapes are utilized for testing purposes. This is because rounded polygons should possess sufficient size to visually demonstrate the curvature during the drawing process. +LARGE_SQUARE = ([0, 0], [300, 0], [300, 300], [0, 300]) +LARGE_DIAMOND = [(100, 300), (300, 500), (500, 300), (300, 100)] +LARGE_RHOMBUS = [(100, 300), (400, 500), (700, 300), (400, 100)] +LARGE_CROSS = ( + [200, 0], + [400, 0], + [400, 200], + [600, 200], + [600, 400], + [400, 400], + [400, 600], + [200, 600], + [200, 400], + [0, 400], + [0, 200], + [200, 200], +) + class DrawPolygonMixin: """Mixin tests for drawing polygons. @@ -4378,6 +4397,103 @@ def test_polygon__valid_border_radius_values(self): self.assertEqual(surface.get_at(pos), expected_color) self.assertIsInstance(bounds_rect, pygame.Rect) + def test_draw_round_square(self): + """Ensure square is drawn with rounded corner""" + surfw = surfh = 1000 + polygon_color = pygame.Color("red") + surface_color = pygame.Color("black") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + self.draw_polygon(surface, polygon_color, LARGE_SQUARE, 1, 5) + + num_points = len(LARGE_SQUARE) + + for i in range(num_points): + pos = x, y = LARGE_SQUARE[i] + x_next, y_next = LARGE_SQUARE[(i + 1) % num_points] + # Calculate the midpoint between the current point + # and the next point in the polygon. + mid = (x + x_next) / 2, (y + y_next) / 2 + # The color at the current vertex should be equal + # to surface_color since the corner is rounded, + # while the color at the mid point of two consecutive + # vertices should always be equal to polygon_color + self.assertEqual(surface.get_at(pos), surface_color) + self.assertEqual(surface.get_at(mid), polygon_color) + + def test_draw_round_diamond(self): + """Ensure diamond is drawn with rounded corner""" + surfw = surfh = 1000 + polygon_color = pygame.Color("red") + surface_color = pygame.Color("black") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + self.draw_polygon(surface, polygon_color, LARGE_DIAMOND, 1, 5) + + num_points = len(LARGE_DIAMOND) + for i in range(num_points): + pos = x, y = LARGE_DIAMOND[i] + x_next, y_next = LARGE_DIAMOND[(i + 1) % num_points] + # Calculate the midpoint between the current point + # and the next point in the polygon. + mid = (x + x_next) / 2, (y + y_next) / 2 + # The color at the current vertex should be equal + # to surface_color since the corner is rounded, + # while the color at the mid point of two consecutive + # vertices should always be equal to polygon_color + self.assertEqual(surface.get_at(pos), surface_color) + self.assertEqual(surface.get_at(mid), polygon_color) + + def test_draw_round_rhombus(self): + """Ensure rhombus is drawn with rounded corner""" + surfw = surfh = 1000 + polygon_color = pygame.Color("red") + surface_color = pygame.Color("black") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + self.draw_polygon(surface, polygon_color, LARGE_RHOMBUS, 1, 5) + + num_points = len(LARGE_RHOMBUS) + for i in range(num_points): + pos = x, y = LARGE_RHOMBUS[i] + x_next, y_next = LARGE_RHOMBUS[(i + 1) % num_points] + # Calculate the midpoint between the current point + # and the next point in the polygon. + mid = (x + x_next) / 2, (y + y_next) / 2 + # The color at the current vertex should be equal + # to surface_color since the corner is rounded, + # while the color at the mid point of two consecutive + # vertices should always be equal to polygon_color + self.assertEqual(surface.get_at(pos), surface_color) + self.assertEqual(surface.get_at(mid), polygon_color) + + def test_draw_round_cross(self): + """Ensure cross is drawn with rounded corner""" + surfw = surfh = 1000 + polygon_color = pygame.Color("red") + surface_color = pygame.Color("black") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + self.draw_polygon(surface, polygon_color, LARGE_CROSS, 1, 5) + + num_points = len(LARGE_CROSS) + for i in range(num_points): + pos = x, y = LARGE_CROSS[i] + x_next, y_next = LARGE_CROSS[(i + 1) % num_points] + # Calculate the midpoint between the current point + # and the next point in the polygon. + mid = (x + x_next) / 2, (y + y_next) / 2 + # The color at the current vertex should be equal + # to surface_color since the corner is rounded, + # while the color at the mid point of two consecutive + # vertices should always be equal to polygon_color + self.assertEqual(surface.get_at(pos), surface_color) + self.assertEqual(surface.get_at(mid), polygon_color) + class DrawPolygonTest(DrawPolygonMixin, DrawTestCase): """Test draw module function polygon.