From 1932b82552bae33651b9b35382dcd4104602dcc9 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Wed, 16 Oct 2024 14:29:13 +0200 Subject: [PATCH 01/11] Changes to be commited: new file: test.txt --- test.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test.txt diff --git a/test.txt b/test.txt new file mode 100644 index 0000000000..e69de29bb2 From 49dbee5fd853456b345f7c210ec41b2c989badd4 Mon Sep 17 00:00:00 2001 From: GabrieleLS Date: Wed, 16 Oct 2024 14:30:57 +0200 Subject: [PATCH 02/11] Delete test.txt --- test.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test.txt diff --git a/test.txt b/test.txt deleted file mode 100644 index e69de29bb2..0000000000 From 131d95696edbf90eec611363456b8142290fc98c Mon Sep 17 00:00:00 2001 From: AntoineMamou Date: Tue, 12 Nov 2024 18:24:11 +0100 Subject: [PATCH 03/11] add angle and angle_rad property for vector2 --- src_c/math.c | 47 +++++++++++++++++++++++++++++++++++++ test/math_test.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/src_c/math.c b/src_c/math.c index 24c04365ca..1af50fb9e7 100644 --- a/src_c/math.c +++ b/src_c/math.c @@ -201,7 +201,10 @@ static int vector_sety(pgVector *self, PyObject *value, void *closure); static int vector_setz(pgVector *self, PyObject *value, void *closure); +static PyObject *static PyObject * +vector_get_angle(pgVector *self, void *closure); static PyObject * +vector_get_angle_rad(pgVector *self, void *closure); vector_richcompare(PyObject *o1, PyObject *o2, int op); static PyObject * vector_length(pgVector *self, PyObject *args); @@ -1269,6 +1272,48 @@ vector_setz(pgVector *self, PyObject *value, void *closure) return vector_set_component(self, value, 2); } +static PyObject * +vector_get_angle(pgVector *self, void *closure) +{ + pgVector *vec = (pgVector *)self; + + if (vec->coords[0] == 0.0 && vec->coords[1] == 0.0) { + return PyFloat_FromDouble(0.0); + } + + double angle = atan2(vec->coords[1], vec->coords[0]) * 180.0 / M_PI; + + if (angle > 180.0) { + angle -= 360.0; + } + else if (angle <= -180.0) { + angle += 360.0; + } + + return PyFloat_FromDouble(angle); +} + +static PyObject * +vector_get_angle_rad(pgVector *self, void *closure) +{ + pgVector *vec = (pgVector *)self; + + if (vec->coords[0] == 0.0 && vec->coords[1] == 0.0) { + return PyFloat_FromDouble(0.0); + } + + double angle_rad = atan2(vec->coords[1], vec->coords[0]); + + if (angle_rad > M_PI) { + angle_rad -= 2 * M_PI; + } + else if (angle_rad <= -M_PI) { + angle_rad += 2 * M_PI; + } + + return PyFloat_FromDouble(angle_rad); +} + static PyObject * vector_richcompare(PyObject *o1, PyObject *o2, int op) { @@ -2585,6 +2630,8 @@ static PyMethodDef vector2_methods[] = { static PyGetSetDef vector2_getsets[] = { {"x", (getter)vector_getx, (setter)vector_setx, NULL, NULL}, {"y", (getter)vector_gety, (setter)vector_sety, NULL, NULL}, + {"angle", (getter)vector_get_angle, NULL, NULL, NULL}, + {"angle_rad", (getter)vector_get_angle_rad, NULL, NULL, NULL}, {NULL, 0, NULL, NULL, NULL} /* Sentinel */ }; diff --git a/test/math_test.py b/test/math_test.py index d8690ff502..9b39d1346f 100644 --- a/test/math_test.py +++ b/test/math_test.py @@ -1363,6 +1363,66 @@ def test_del_y(self): exception = ctx.exception self.assertEqual(str(exception), "Cannot delete the y attribute") + def test_vector_get_angle_zero_vector(self): + vec = Vector2(0, 0) + self.assertEqual(vec.angle, 0.0) + + def test_vector_get_angle_rad_zero_vector(self): + vec = Vector2(0, 0) + self.assertEqual(vec.angle_rad, 0.0) + + def test_vector_get_angle_on_axes(self): + vec1 = Vector2(1, 0) + self.assertEqual(vec1.angle, 0.0) + + vec2 = Vector2(0, 1) + self.assertEqual(vec2.angle, 90.0) + + vec3 = Vector2(-1, 0) + self.assertEqual(vec3.angle, 180.0) + + vec4 = Vector2(0, -1) + self.assertEqual(vec4.angle, -90.0) + + def test_vector_get_angle_rad_on_axes(self): + vec1 = Vector2(1, 0) + self.assertEqual(vec1.angle_rad, 0.0) + + vec2 = Vector2(0, 1) + self.assertEqual(vec2.angle_rad, math.pi / 2) + + vec3 = Vector2(-1, 0) + self.assertEqual(vec3.angle_rad, math.pi) + + vec4 = Vector2(0, -1) + self.assertEqual(vec4.angle_rad, -math.pi / 2) + + def test_vector_get_angle_in_quadrants(self): + vec1 = Vector2(1, 1) + self.assertEqual(vec1.angle, 45.0) + + vec2 = Vector2(-1, 1) + self.assertEqual(vec2.angle, 135.0) + + vec3 = Vector2(-1, -1) + self.assertEqual(vec3.angle, -135.0) + + vec4 = Vector2(1, -1) + self.assertEqual(vec4.angle, -45.0) + + def test_vector_get_angle_rad_in_quadrants(self): + vec1 = Vector2(1, 1) + self.assertEqual(vec1.angle_rad, math.pi / 4) + + vec2 = Vector2(-1, 1) + self.assertEqual(vec2.angle_rad, 3 * math.pi / 4) + + vec3 = Vector2(-1, -1) + self.assertEqual(vec3.angle_rad, -3 * math.pi / 4) + + vec4 = Vector2(1, -1) + self.assertEqual(vec4.angle_rad, -math.pi / 4) + class Vector3TypeTest(unittest.TestCase): def setUp(self): From 91fe678894416a102a5b4ea576eaf3124c13b6e4 Mon Sep 17 00:00:00 2001 From: AntoineMamou Date: Tue, 12 Nov 2024 18:39:27 +0100 Subject: [PATCH 04/11] syntax error correction --- src_c/math.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src_c/math.c b/src_c/math.c index 1af50fb9e7..c2dfc228f4 100644 --- a/src_c/math.c +++ b/src_c/math.c @@ -201,10 +201,11 @@ static int vector_sety(pgVector *self, PyObject *value, void *closure); static int vector_setz(pgVector *self, PyObject *value, void *closure); -static PyObject *static PyObject * +static PyObject * vector_get_angle(pgVector *self, void *closure); static PyObject * vector_get_angle_rad(pgVector *self, void *closure); +static PyObject * vector_richcompare(PyObject *o1, PyObject *o2, int op); static PyObject * vector_length(pgVector *self, PyObject *args); From faf77a80780309be068aacb0f3aa3d079c91f20e Mon Sep 17 00:00:00 2001 From: AntoineMamou Date: Wed, 13 Nov 2024 18:53:49 +0100 Subject: [PATCH 05/11] code modification for properties angle and angle rad and new doc for these properties --- docs/reST/ref/math.rst | 20 ++++++++++++++++++++ src_c/doc/math_doc.h | 2 ++ src_c/math.c | 34 ++++++++++++++++++++-------------- test/math_test.py | 16 ++++++++++++++-- 4 files changed, 56 insertions(+), 16 deletions(-) diff --git a/docs/reST/ref/math.rst b/docs/reST/ref/math.rst index e359a4b899..24a2376b32 100644 --- a/docs/reST/ref/math.rst +++ b/docs/reST/ref/math.rst @@ -617,6 +617,26 @@ Multiple coordinates can be set using slices or swizzling might help you out. + .. attribute:: angle + + | :sl:`Gives the angle of the vector in degrees, relative to the X-axis, normalized to the interval (-180, 180].` + + Read-only attribute representing the angle of the vector in degrees relative to the X-axis. This angle is normalized to + the interval (-180, 180]. If accessed for the zero vector (0, 0), an exception is raised, as the angle is undefined for + a zero-length vector. + + Usage: Accessing `angle` provides the current angle of the vector in degrees within the specified range. + + .. attribute:: angle_rad + + | :sl:`Gives the angle of the vector in radians, relative to the X-axis, normalized to the interval (-π, π].` + + Read-only attribute representing the angle of the vector in radians relative to the X-axis. This value is equivalent + to the `angle` attribute converted to radians and is normalized to the interval (-π, π]. If accessed for the zero vector + (0, 0), an exception is raised, as the angle is undefined for a zero-length vector. + + Usage: Accessing `angle_rad` provides the current angle of the vector in radians within the specified range. + .. ## pygame.math.Vector2 ## .. class:: Vector3 diff --git a/src_c/doc/math_doc.h b/src_c/doc/math_doc.h index e002a0c672..75f06f4a5e 100644 --- a/src_c/doc/math_doc.h +++ b/src_c/doc/math_doc.h @@ -40,6 +40,8 @@ #define DOC_MATH_VECTOR2_CLAMPMAGNITUDEIP "clamp_magnitude_ip(max_length, /) -> None\nclamp_magnitude_ip(min_length, max_length, /) -> None\nClamps the vector's magnitude between max_length and min_length" #define DOC_MATH_VECTOR2_UPDATE "update() -> None\nupdate(int) -> None\nupdate(float) -> None\nupdate(Vector2) -> None\nupdate(x, y) -> None\nupdate((x, y)) -> None\nSets the coordinates of the vector." #define DOC_MATH_VECTOR2_EPSILON "Determines the tolerance of vector calculations." +#define DOC_MATH_VECTOR2_ANGLE "Gives the angle of the vector in degrees, relative to the X-axis, normalized to the interval (-180, 180]." +#define DOC_MATH_VECTOR2_ANGLERAD "Gives the angle of the vector in radians, relative to the X-axis, normalized to the interval (-π, π]." #define DOC_MATH_VECTOR3 "Vector3() -> Vector3(0, 0, 0)\nVector3(int) -> Vector3\nVector3(float) -> Vector3\nVector3(Vector3) -> Vector3\nVector3(x, y, z) -> Vector3\nVector3((x, y, z)) -> Vector3\na 3-Dimensional Vector" #define DOC_MATH_VECTOR3_DOT "dot(Vector3, /) -> float\ncalculates the dot- or scalar-product with the other vector" #define DOC_MATH_VECTOR3_CROSS "cross(Vector3, /) -> Vector3\ncalculates the cross- or vector-product" diff --git a/src_c/math.c b/src_c/math.c index c2dfc228f4..541fe71867 100644 --- a/src_c/math.c +++ b/src_c/math.c @@ -51,6 +51,9 @@ #define TWO_PI (2. * M_PI) +#define RAD_TO_DEG (180.0 / M_PI) +#define DEG_TO_RAD (M_PI / 180.0) + #ifndef M_PI_2 #define M_PI_2 (M_PI / 2.0) #endif /* M_PI_2 */ @@ -1276,13 +1279,15 @@ vector_setz(pgVector *self, PyObject *value, void *closure) static PyObject * vector_get_angle(pgVector *self, void *closure) { - pgVector *vec = (pgVector *)self; + pgVector *vec = self; if (vec->coords[0] == 0.0 && vec->coords[1] == 0.0) { - return PyFloat_FromDouble(0.0); + PyErr_SetString(PyExc_ValueError, + "Angle is undefined for the zero vector (0, 0)"); + return NULL; } - double angle = atan2(vec->coords[1], vec->coords[0]) * 180.0 / M_PI; + double angle = atan2(vec->coords[1], vec->coords[0]) * RAD_TO_DEG; if (angle > 180.0) { angle -= 360.0; @@ -1297,20 +1302,20 @@ vector_get_angle(pgVector *self, void *closure) static PyObject * vector_get_angle_rad(pgVector *self, void *closure) { - pgVector *vec = (pgVector *)self; + pgVector *vec = self; if (vec->coords[0] == 0.0 && vec->coords[1] == 0.0) { - return PyFloat_FromDouble(0.0); + PyErr_SetString(PyExc_ValueError, + "Angle is undefined for the zero vector (0, 0)"); + return NULL; } - double angle_rad = atan2(vec->coords[1], vec->coords[0]); + PyObject *angle_obj = vector_get_angle(self, closure); + double angle_deg = PyFloat_AsDouble(angle_obj); - if (angle_rad > M_PI) { - angle_rad -= 2 * M_PI; - } - else if (angle_rad <= -M_PI) { - angle_rad += 2 * M_PI; - } + double angle_rad = angle_deg * DEG_TO_RAD; + + Py_XDECREF(angle_obj); return PyFloat_FromDouble(angle_rad); } @@ -2631,8 +2636,9 @@ static PyMethodDef vector2_methods[] = { static PyGetSetDef vector2_getsets[] = { {"x", (getter)vector_getx, (setter)vector_setx, NULL, NULL}, {"y", (getter)vector_gety, (setter)vector_sety, NULL, NULL}, - {"angle", (getter)vector_get_angle, NULL, NULL, NULL}, - {"angle_rad", (getter)vector_get_angle_rad, NULL, NULL, NULL}, + {"angle", (getter)vector_get_angle, NULL, DOC_MATH_VECTOR2_ANGLE, NULL}, + {"angle_rad", (getter)vector_get_angle_rad, NULL, + DOC_MATH_VECTOR2_ANGLERAD, NULL}, {NULL, 0, NULL, NULL, NULL} /* Sentinel */ }; diff --git a/test/math_test.py b/test/math_test.py index 9b39d1346f..7081638147 100644 --- a/test/math_test.py +++ b/test/math_test.py @@ -1365,11 +1365,23 @@ def test_del_y(self): def test_vector_get_angle_zero_vector(self): vec = Vector2(0, 0) - self.assertEqual(vec.angle, 0.0) + with self.assertRaises(ValueError) as e: + vec.angle + + exception = e.exception + self.assertEqual( + str(exception), "Angle is undefined for the zero vector (0, 0)" + ) def test_vector_get_angle_rad_zero_vector(self): vec = Vector2(0, 0) - self.assertEqual(vec.angle_rad, 0.0) + with self.assertRaises(ValueError) as e: + vec.angle_rad + + exception = e.exception + self.assertEqual( + str(exception), "Angle is undefined for the zero vector (0, 0)" + ) def test_vector_get_angle_on_axes(self): vec1 = Vector2(1, 0) From 0f6dc9ab9d8f14ba207fe45c9723ba6f3031f922 Mon Sep 17 00:00:00 2001 From: AntoineMamou Date: Sat, 16 Nov 2024 15:18:10 +0100 Subject: [PATCH 06/11] type stubs correction --- buildconfig/stubs/pygame/math.pyi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/buildconfig/stubs/pygame/math.pyi b/buildconfig/stubs/pygame/math.pyi index 4526960b5a..b08b7b6b51 100644 --- a/buildconfig/stubs/pygame/math.pyi +++ b/buildconfig/stubs/pygame/math.pyi @@ -221,6 +221,8 @@ class Vector2(_GenericVector): xy: Vector2 yx: Vector2 yy: Vector2 + angle: float + angle_rad: float @overload def __init__( self: _TVec, From 4d66c2ae64135c20d8282ec3960c4da59ba21f2e Mon Sep 17 00:00:00 2001 From: AntoineMamou Date: Sat, 16 Nov 2024 18:59:23 +0100 Subject: [PATCH 07/11] add new property vector2_default_angle --- buildconfig/stubs/pygame/math.pyi | 1 + docs/reST/ref/math.rst | 14 ++++++++--- src_c/doc/math_doc.h | 1 + src_c/math.c | 42 ++++++++++++++++++++++++++----- 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/buildconfig/stubs/pygame/math.pyi b/buildconfig/stubs/pygame/math.pyi index b08b7b6b51..0795b53c1a 100644 --- a/buildconfig/stubs/pygame/math.pyi +++ b/buildconfig/stubs/pygame/math.pyi @@ -223,6 +223,7 @@ class Vector2(_GenericVector): yy: Vector2 angle: float angle_rad: float + vector2_default_angle : float @overload def __init__( self: _TVec, diff --git a/docs/reST/ref/math.rst b/docs/reST/ref/math.rst index 24a2376b32..b530f5ad37 100644 --- a/docs/reST/ref/math.rst +++ b/docs/reST/ref/math.rst @@ -616,14 +616,22 @@ Multiple coordinates can be set using slices or swizzling find that either the margin is too large or too small, in which case changing ``epsilon`` slightly might help you out. + .. attribute:: vector2_default_angle + + | :sl:`Gives the default angle of the vector in degrees, relative to the X-axis.` + + Read-write attribute representing the default angle of the vector in degrees relative to the X-axis. + + Usage: + - Accessing `vector2_default_angle` provides the current default angle of the vector in degrees. + - Setting `vector2_default_angle` allows you to specify a default angle for zero-length vectors. .. attribute:: angle | :sl:`Gives the angle of the vector in degrees, relative to the X-axis, normalized to the interval (-180, 180].` Read-only attribute representing the angle of the vector in degrees relative to the X-axis. This angle is normalized to - the interval (-180, 180]. If accessed for the zero vector (0, 0), an exception is raised, as the angle is undefined for - a zero-length vector. + the interval (-180, 180]. If accessed for the zero vector (0, 0), `vector2_default_angle` is returned. Usage: Accessing `angle` provides the current angle of the vector in degrees within the specified range. @@ -633,7 +641,7 @@ Multiple coordinates can be set using slices or swizzling Read-only attribute representing the angle of the vector in radians relative to the X-axis. This value is equivalent to the `angle` attribute converted to radians and is normalized to the interval (-π, π]. If accessed for the zero vector - (0, 0), an exception is raised, as the angle is undefined for a zero-length vector. + (0, 0), `vector2_default_angle` is returned. Usage: Accessing `angle_rad` provides the current angle of the vector in radians within the specified range. diff --git a/src_c/doc/math_doc.h b/src_c/doc/math_doc.h index 75f06f4a5e..2a78be9186 100644 --- a/src_c/doc/math_doc.h +++ b/src_c/doc/math_doc.h @@ -40,6 +40,7 @@ #define DOC_MATH_VECTOR2_CLAMPMAGNITUDEIP "clamp_magnitude_ip(max_length, /) -> None\nclamp_magnitude_ip(min_length, max_length, /) -> None\nClamps the vector's magnitude between max_length and min_length" #define DOC_MATH_VECTOR2_UPDATE "update() -> None\nupdate(int) -> None\nupdate(float) -> None\nupdate(Vector2) -> None\nupdate(x, y) -> None\nupdate((x, y)) -> None\nSets the coordinates of the vector." #define DOC_MATH_VECTOR2_EPSILON "Determines the tolerance of vector calculations." +#define DOC_MATH_VECTOR2_VECTOR2DEFAULTANGLE "Gives the default angle of the vector in degrees, relative to the X-axis." #define DOC_MATH_VECTOR2_ANGLE "Gives the angle of the vector in degrees, relative to the X-axis, normalized to the interval (-180, 180]." #define DOC_MATH_VECTOR2_ANGLERAD "Gives the angle of the vector in radians, relative to the X-axis, normalized to the interval (-π, π]." #define DOC_MATH_VECTOR3 "Vector3() -> Vector3(0, 0, 0)\nVector3(int) -> Vector3\nVector3(float) -> Vector3\nVector3(Vector3) -> Vector3\nVector3(x, y, z) -> Vector3\nVector3((x, y, z)) -> Vector3\na 3-Dimensional Vector" diff --git a/src_c/math.c b/src_c/math.c index 541fe71867..af80ef555c 100644 --- a/src_c/math.c +++ b/src_c/math.c @@ -99,6 +99,8 @@ static PyTypeObject pgVectorIter_Type; #define DEG2RAD(angle) ((angle) * M_PI / 180.) #define RAD2DEG(angle) ((angle) * 180. / M_PI) +static double vector2_default_angle = NAN; + typedef struct { PyObject_HEAD double coords[VECTOR_MAX_SIZE]; /* Coordinates */ Py_ssize_t dim; /* Dimension of the vector */ @@ -145,6 +147,10 @@ _vector_coords_from_string(PyObject *str, char **delimiter, double *coords, static void _vector_move_towards_helper(Py_ssize_t dim, double *origin_coords, double *target_coords, double max_distance); +static PyObject * +math_get_vector2_default_angle(PyObject *self, void *closure); +static int +math_set_vector2_default_angle(PyObject *self, PyObject *arg, void *closure); /* generic vector functions */ static PyObject * @@ -1282,9 +1288,7 @@ vector_get_angle(pgVector *self, void *closure) pgVector *vec = self; if (vec->coords[0] == 0.0 && vec->coords[1] == 0.0) { - PyErr_SetString(PyExc_ValueError, - "Angle is undefined for the zero vector (0, 0)"); - return NULL; + return PyFloat_FromDouble(vector2_default_angle); } double angle = atan2(vec->coords[1], vec->coords[0]) * RAD_TO_DEG; @@ -1305,9 +1309,7 @@ vector_get_angle_rad(pgVector *self, void *closure) pgVector *vec = self; if (vec->coords[0] == 0.0 && vec->coords[1] == 0.0) { - PyErr_SetString(PyExc_ValueError, - "Angle is undefined for the zero vector (0, 0)"); - return NULL; + return PyFloat_FromDouble(vector2_default_angle); } PyObject *angle_obj = vector_get_angle(self, closure); @@ -2639,6 +2641,8 @@ static PyGetSetDef vector2_getsets[] = { {"angle", (getter)vector_get_angle, NULL, DOC_MATH_VECTOR2_ANGLE, NULL}, {"angle_rad", (getter)vector_get_angle_rad, NULL, DOC_MATH_VECTOR2_ANGLERAD, NULL}, + {"vector2_default_angle", (getter)math_get_vector2_default_angle, + (setter)math_set_vector2_default_angle, DOC_MATH_VECTOR2_ANGLE, NULL}, {NULL, 0, NULL, NULL, NULL} /* Sentinel */ }; @@ -4452,6 +4456,32 @@ math_disable_swizzling(pgVector *self, PyObject *_null) Py_RETURN_NONE; } +static PyObject * +math_get_vector2_default_angle(PyObject *self, void *closure) +{ + return PyFloat_FromDouble(vector2_default_angle); +} + +static int +math_set_vector2_default_angle(PyObject *self, PyObject *arg, void *closure) +{ + if (arg == NULL) { + PyErr_SetString(PyExc_TypeError, + "math.Vector2.default_angle cannot be deleted"); + return -1; + } + + if (!PyFloat_Check(arg)) { + PyErr_SetString(PyExc_TypeError, + "math.Vector2.default_angle must be a float"); + return -1; + } + + vector2_default_angle = PyFloat_AsDouble(arg); + + return 0; +} + static PyMethodDef _math_methods[] = { {"clamp", (PyCFunction)math_clamp, METH_FASTCALL, DOC_MATH_CLAMP}, {"lerp", (PyCFunction)math_lerp, METH_FASTCALL, DOC_MATH_LERP}, From ad6f04faa108fc05572d6f36c12f05f6d60844cf Mon Sep 17 00:00:00 2001 From: AntoineMamou Date: Sat, 16 Nov 2024 19:11:50 +0100 Subject: [PATCH 08/11] correction math_test.py --- test/math_test.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/test/math_test.py b/test/math_test.py index 7081638147..a054c55576 100644 --- a/test/math_test.py +++ b/test/math_test.py @@ -1365,22 +1365,12 @@ def test_del_y(self): def test_vector_get_angle_zero_vector(self): vec = Vector2(0, 0) - with self.assertRaises(ValueError) as e: - vec.angle - - exception = e.exception - self.assertEqual( - str(exception), "Angle is undefined for the zero vector (0, 0)" - ) + self.assertTrue(math.isnan(vec.angle) and math.isnan(vec.vector2_default_angle)) def test_vector_get_angle_rad_zero_vector(self): vec = Vector2(0, 0) - with self.assertRaises(ValueError) as e: - vec.angle_rad - - exception = e.exception - self.assertEqual( - str(exception), "Angle is undefined for the zero vector (0, 0)" + self.assertTrue( + math.isnan(vec.angle_rad) and math.isnan(vec.vector2_default_angle) ) def test_vector_get_angle_on_axes(self): From 71c12a75aeb16fd77be78359b5f8c83be19eade9 Mon Sep 17 00:00:00 2001 From: AntoineMamou Date: Mon, 18 Nov 2024 18:07:30 +0100 Subject: [PATCH 09/11] .angle and .angle_rad with math.atan2 behaviour for special cases --- buildconfig/stubs/pygame/math.pyi | 1 - docs/reST/ref/math.rst | 15 +--- src_c/doc/math_doc.h | 1 - src_c/math.c | 83 +++++++------------- test/math_test.py | 124 +++++++++++++++++++----------- 5 files changed, 110 insertions(+), 114 deletions(-) diff --git a/buildconfig/stubs/pygame/math.pyi b/buildconfig/stubs/pygame/math.pyi index 0795b53c1a..b08b7b6b51 100644 --- a/buildconfig/stubs/pygame/math.pyi +++ b/buildconfig/stubs/pygame/math.pyi @@ -223,7 +223,6 @@ class Vector2(_GenericVector): yy: Vector2 angle: float angle_rad: float - vector2_default_angle : float @overload def __init__( self: _TVec, diff --git a/docs/reST/ref/math.rst b/docs/reST/ref/math.rst index b530f5ad37..fb7db3cadf 100644 --- a/docs/reST/ref/math.rst +++ b/docs/reST/ref/math.rst @@ -616,22 +616,12 @@ Multiple coordinates can be set using slices or swizzling find that either the margin is too large or too small, in which case changing ``epsilon`` slightly might help you out. - .. attribute:: vector2_default_angle - - | :sl:`Gives the default angle of the vector in degrees, relative to the X-axis.` - - Read-write attribute representing the default angle of the vector in degrees relative to the X-axis. - - Usage: - - Accessing `vector2_default_angle` provides the current default angle of the vector in degrees. - - Setting `vector2_default_angle` allows you to specify a default angle for zero-length vectors. - .. attribute:: angle | :sl:`Gives the angle of the vector in degrees, relative to the X-axis, normalized to the interval (-180, 180].` Read-only attribute representing the angle of the vector in degrees relative to the X-axis. This angle is normalized to - the interval (-180, 180]. If accessed for the zero vector (0, 0), `vector2_default_angle` is returned. + the interval (-180, 180]. Usage: Accessing `angle` provides the current angle of the vector in degrees within the specified range. @@ -640,8 +630,7 @@ Multiple coordinates can be set using slices or swizzling | :sl:`Gives the angle of the vector in radians, relative to the X-axis, normalized to the interval (-π, π].` Read-only attribute representing the angle of the vector in radians relative to the X-axis. This value is equivalent - to the `angle` attribute converted to radians and is normalized to the interval (-π, π]. If accessed for the zero vector - (0, 0), `vector2_default_angle` is returned. + to the `angle` attribute converted to radians and is normalized to the interval (-π, π]. Usage: Accessing `angle_rad` provides the current angle of the vector in radians within the specified range. diff --git a/src_c/doc/math_doc.h b/src_c/doc/math_doc.h index 2a78be9186..75f06f4a5e 100644 --- a/src_c/doc/math_doc.h +++ b/src_c/doc/math_doc.h @@ -40,7 +40,6 @@ #define DOC_MATH_VECTOR2_CLAMPMAGNITUDEIP "clamp_magnitude_ip(max_length, /) -> None\nclamp_magnitude_ip(min_length, max_length, /) -> None\nClamps the vector's magnitude between max_length and min_length" #define DOC_MATH_VECTOR2_UPDATE "update() -> None\nupdate(int) -> None\nupdate(float) -> None\nupdate(Vector2) -> None\nupdate(x, y) -> None\nupdate((x, y)) -> None\nSets the coordinates of the vector." #define DOC_MATH_VECTOR2_EPSILON "Determines the tolerance of vector calculations." -#define DOC_MATH_VECTOR2_VECTOR2DEFAULTANGLE "Gives the default angle of the vector in degrees, relative to the X-axis." #define DOC_MATH_VECTOR2_ANGLE "Gives the angle of the vector in degrees, relative to the X-axis, normalized to the interval (-180, 180]." #define DOC_MATH_VECTOR2_ANGLERAD "Gives the angle of the vector in radians, relative to the X-axis, normalized to the interval (-π, π]." #define DOC_MATH_VECTOR3 "Vector3() -> Vector3(0, 0, 0)\nVector3(int) -> Vector3\nVector3(float) -> Vector3\nVector3(Vector3) -> Vector3\nVector3(x, y, z) -> Vector3\nVector3((x, y, z)) -> Vector3\na 3-Dimensional Vector" diff --git a/src_c/math.c b/src_c/math.c index af80ef555c..4025b7bfc9 100644 --- a/src_c/math.c +++ b/src_c/math.c @@ -99,8 +99,6 @@ static PyTypeObject pgVectorIter_Type; #define DEG2RAD(angle) ((angle) * M_PI / 180.) #define RAD2DEG(angle) ((angle) * 180. / M_PI) -static double vector2_default_angle = NAN; - typedef struct { PyObject_HEAD double coords[VECTOR_MAX_SIZE]; /* Coordinates */ Py_ssize_t dim; /* Dimension of the vector */ @@ -147,11 +145,6 @@ _vector_coords_from_string(PyObject *str, char **delimiter, double *coords, static void _vector_move_towards_helper(Py_ssize_t dim, double *origin_coords, double *target_coords, double max_distance); -static PyObject * -math_get_vector2_default_angle(PyObject *self, void *closure); -static int -math_set_vector2_default_angle(PyObject *self, PyObject *arg, void *closure); - /* generic vector functions */ static PyObject * pgVector_NEW(Py_ssize_t dim); @@ -1285,41 +1278,49 @@ vector_setz(pgVector *self, PyObject *value, void *closure) static PyObject * vector_get_angle(pgVector *self, void *closure) { - pgVector *vec = self; - - if (vec->coords[0] == 0.0 && vec->coords[1] == 0.0) { - return PyFloat_FromDouble(vector2_default_angle); - } - - double angle = atan2(vec->coords[1], vec->coords[0]) * RAD_TO_DEG; + PyObject *angle_obj = vector_get_angle_rad(self, closure); + double angle_rad = PyFloat_AsDouble(angle_obj); + double angle_deg = angle_rad * RAD_TO_DEG; - if (angle > 180.0) { - angle -= 360.0; + if (angle_deg > 180.0) { + angle_deg -= 360.0; } - else if (angle <= -180.0) { - angle += 360.0; + else if (angle_deg <= -180.0) { + angle_deg += 360.0; } - return PyFloat_FromDouble(angle); + return PyFloat_FromDouble(angle_deg); } static PyObject * vector_get_angle_rad(pgVector *self, void *closure) { pgVector *vec = self; + double x = vec->coords[0]; + double y = vec->coords[1]; - if (vec->coords[0] == 0.0 && vec->coords[1] == 0.0) { - return PyFloat_FromDouble(vector2_default_angle); + if (Py_IS_NAN(x) || Py_IS_NAN(y)) { + return PyFloat_FromDouble(Py_NAN); } - PyObject *angle_obj = vector_get_angle(self, closure); - double angle_deg = PyFloat_AsDouble(angle_obj); - - double angle_rad = angle_deg * DEG_TO_RAD; + if (Py_IS_INFINITY(y)) { + if (Py_IS_INFINITY(x)) { + if (copysign(1., x) == 1.) + return PyFloat_FromDouble(copysign(0.25 * Py_MATH_PI, y)); + else + return PyFloat_FromDouble(copysign(0.75 * Py_MATH_PI, y)); + } + return PyFloat_FromDouble(copysign(0.5 * Py_MATH_PI, y)); + } - Py_XDECREF(angle_obj); + if (Py_IS_INFINITY(x) || y == 0.) { + if (copysign(1., x) == 1.) + return PyFloat_FromDouble(copysign(0., y)); + else + return PyFloat_FromDouble(copysign(Py_MATH_PI, y)); + } - return PyFloat_FromDouble(angle_rad); + return PyFloat_FromDouble(atan2(y, x)); } static PyObject * @@ -2641,8 +2642,6 @@ static PyGetSetDef vector2_getsets[] = { {"angle", (getter)vector_get_angle, NULL, DOC_MATH_VECTOR2_ANGLE, NULL}, {"angle_rad", (getter)vector_get_angle_rad, NULL, DOC_MATH_VECTOR2_ANGLERAD, NULL}, - {"vector2_default_angle", (getter)math_get_vector2_default_angle, - (setter)math_set_vector2_default_angle, DOC_MATH_VECTOR2_ANGLE, NULL}, {NULL, 0, NULL, NULL, NULL} /* Sentinel */ }; @@ -4456,32 +4455,6 @@ math_disable_swizzling(pgVector *self, PyObject *_null) Py_RETURN_NONE; } -static PyObject * -math_get_vector2_default_angle(PyObject *self, void *closure) -{ - return PyFloat_FromDouble(vector2_default_angle); -} - -static int -math_set_vector2_default_angle(PyObject *self, PyObject *arg, void *closure) -{ - if (arg == NULL) { - PyErr_SetString(PyExc_TypeError, - "math.Vector2.default_angle cannot be deleted"); - return -1; - } - - if (!PyFloat_Check(arg)) { - PyErr_SetString(PyExc_TypeError, - "math.Vector2.default_angle must be a float"); - return -1; - } - - vector2_default_angle = PyFloat_AsDouble(arg); - - return 0; -} - static PyMethodDef _math_methods[] = { {"clamp", (PyCFunction)math_clamp, METH_FASTCALL, DOC_MATH_CLAMP}, {"lerp", (PyCFunction)math_lerp, METH_FASTCALL, DOC_MATH_LERP}, diff --git a/test/math_test.py b/test/math_test.py index a054c55576..e99f3aa25f 100644 --- a/test/math_test.py +++ b/test/math_test.py @@ -1363,67 +1363,103 @@ def test_del_y(self): exception = ctx.exception self.assertEqual(str(exception), "Cannot delete the y attribute") - def test_vector_get_angle_zero_vector(self): - vec = Vector2(0, 0) - self.assertTrue(math.isnan(vec.angle) and math.isnan(vec.vector2_default_angle)) + def test_angle_rad_property(self): + v0 = Vector2(1, 0) + self.assertEqual(v0.angle_rad, 0.0) - def test_vector_get_angle_rad_zero_vector(self): - vec = Vector2(0, 0) - self.assertTrue( - math.isnan(vec.angle_rad) and math.isnan(vec.vector2_default_angle) - ) + v1 = Vector2(0, 1) + self.assertEqual(v1.angle_rad, math.pi / 2) + + v2 = Vector2(-1, 0) + self.assertEqual(v2.angle_rad, math.pi) + + v3 = Vector2(0, -1) + self.assertEqual(v3.angle_rad, -math.pi / 2) + + v4 = Vector2(1, 1) + self.assertEqual(v4.angle_rad, math.pi / 4) + + v5 = Vector2(-1, 1) + self.assertEqual(v5.angle_rad, 3 * math.pi / 4) + + v6 = Vector2(-1, -1) + self.assertEqual(v6.angle_rad, -3 * math.pi / 4) + + v7 = Vector2(1, -1) + self.assertEqual(v7.angle_rad, -math.pi / 4) + + v8 = Vector2(float('inf'), float('inf')) + self.assertEqual(v8.angle_rad, math.pi / 4) + + v9 = Vector2(float('-inf'), float('inf')) + self.assertEqual(v9.angle_rad, 3 * math.pi / 4) + + v10 = Vector2(float('-inf'), float('-inf')) + self.assertEqual(v10.angle_rad, -3 * math.pi / 4) + + v11 = Vector2(float('inf'), float('-inf')) + self.assertEqual(v11.angle_rad, -math.pi / 4) + + v12 = Vector2(0, 0) + self.assertEqual(v12.angle_rad, 0.0) + + v13 = Vector2(float('nan'), 1) + self.assertTrue(math.isnan(v13.angle_rad)) + + v14 = Vector2(1, float('nan')) + self.assertTrue(math.isnan(v14.angle_rad)) + + v15 = Vector2(float('nan'), float('nan')) + self.assertTrue(math.isnan(v15.angle_rad)) - def test_vector_get_angle_on_axes(self): - vec1 = Vector2(1, 0) - self.assertEqual(vec1.angle, 0.0) + def test_angle_property(self): + v0 = pygame.math.Vector2(1, 0) + self.assertEqual(v0.angle, 0.0) - vec2 = Vector2(0, 1) - self.assertEqual(vec2.angle, 90.0) + v1 = pygame.math.Vector2(0, 1) + self.assertEqual(v1.angle, 90.0) - vec3 = Vector2(-1, 0) - self.assertEqual(vec3.angle, 180.0) + v2 = pygame.math.Vector2(-1, 0) + self.assertEqual(v2.angle, 180.0) - vec4 = Vector2(0, -1) - self.assertEqual(vec4.angle, -90.0) + v3 = pygame.math.Vector2(0, -1) + self.assertEqual(v3.angle, -90.0) - def test_vector_get_angle_rad_on_axes(self): - vec1 = Vector2(1, 0) - self.assertEqual(vec1.angle_rad, 0.0) + v4 = pygame.math.Vector2(1, 1) + self.assertEqual(v4.angle, 45.0) - vec2 = Vector2(0, 1) - self.assertEqual(vec2.angle_rad, math.pi / 2) + v5 = pygame.math.Vector2(-1, 1) + self.assertEqual(v5.angle, 135.0) - vec3 = Vector2(-1, 0) - self.assertEqual(vec3.angle_rad, math.pi) + v6 = pygame.math.Vector2(-1, -1) + self.assertEqual(v6.angle, -135.0) - vec4 = Vector2(0, -1) - self.assertEqual(vec4.angle_rad, -math.pi / 2) + v7 = pygame.math.Vector2(1, -1) + self.assertEqual(v7.angle, -45.0) - def test_vector_get_angle_in_quadrants(self): - vec1 = Vector2(1, 1) - self.assertEqual(vec1.angle, 45.0) + v8 = pygame.math.Vector2(float('inf'), float('inf')) + self.assertEqual(v8.angle, 45.0) - vec2 = Vector2(-1, 1) - self.assertEqual(vec2.angle, 135.0) + v9 = pygame.math.Vector2(float('-inf'), float('inf')) + self.assertEqual(v9.angle, 135.0) - vec3 = Vector2(-1, -1) - self.assertEqual(vec3.angle, -135.0) + v10 = pygame.math.Vector2(float('-inf'), float('-inf')) + self.assertEqual(v10.angle, -135.0) - vec4 = Vector2(1, -1) - self.assertEqual(vec4.angle, -45.0) + v11 = pygame.math.Vector2(float('inf'), float('-inf')) + self.assertEqual(v11.angle, -45.0) - def test_vector_get_angle_rad_in_quadrants(self): - vec1 = Vector2(1, 1) - self.assertEqual(vec1.angle_rad, math.pi / 4) + v12 = pygame.math.Vector2(0, 0) + self.assertEqual(v12.angle, 0.0) - vec2 = Vector2(-1, 1) - self.assertEqual(vec2.angle_rad, 3 * math.pi / 4) + v13 = pygame.math.Vector2(float('nan'), 1) + self.assertTrue(math.isnan(v13.angle)) - vec3 = Vector2(-1, -1) - self.assertEqual(vec3.angle_rad, -3 * math.pi / 4) + v14 = pygame.math.Vector2(1, float('nan')) + self.assertTrue(math.isnan(v14.angle)) - vec4 = Vector2(1, -1) - self.assertEqual(vec4.angle_rad, -math.pi / 4) + v15 = pygame.math.Vector2(float('nan'), float('nan')) + self.assertTrue(math.isnan(v15.angle)) class Vector3TypeTest(unittest.TestCase): From 23f426f7181081fa37ab2f3a9e774fb2948efa34 Mon Sep 17 00:00:00 2001 From: AntoineMamou Date: Sat, 23 Nov 2024 18:42:02 +0100 Subject: [PATCH 10/11] adding helper function _pg_atan2 + docs modification --- buildconfig/stubs/pygame/math.pyi | 6 ++- docs/reST/ref/math.rst | 12 ++--- src_c/doc/math_doc.h | 4 +- src_c/math.c | 82 +++++++++++++++++-------------- 4 files changed, 56 insertions(+), 48 deletions(-) diff --git a/buildconfig/stubs/pygame/math.pyi b/buildconfig/stubs/pygame/math.pyi index b08b7b6b51..97fb7155dd 100644 --- a/buildconfig/stubs/pygame/math.pyi +++ b/buildconfig/stubs/pygame/math.pyi @@ -221,8 +221,10 @@ class Vector2(_GenericVector): xy: Vector2 yx: Vector2 yy: Vector2 - angle: float - angle_rad: float + @property + def angle(self) -> float: ... + @property + def angle_rad(self) -> float: ... @overload def __init__( self: _TVec, diff --git a/docs/reST/ref/math.rst b/docs/reST/ref/math.rst index fb7db3cadf..0580c5ee81 100644 --- a/docs/reST/ref/math.rst +++ b/docs/reST/ref/math.rst @@ -618,21 +618,21 @@ Multiple coordinates can be set using slices or swizzling .. attribute:: angle - | :sl:`Gives the angle of the vector in degrees, relative to the X-axis, normalized to the interval (-180, 180].` + | :sl:`Gives the angle of the vector in degrees, relative to the X-axis, normalized to the interval [-180, 180].` Read-only attribute representing the angle of the vector in degrees relative to the X-axis. This angle is normalized to - the interval (-180, 180]. + the interval [-180, 180]. - Usage: Accessing `angle` provides the current angle of the vector in degrees within the specified range. + Usage: Accessing `angle` provides the current angle of the vector in degrees within the predefined range of [-180, 180]. .. attribute:: angle_rad - | :sl:`Gives the angle of the vector in radians, relative to the X-axis, normalized to the interval (-π, π].` + | :sl:`Gives the angle of the vector in radians, relative to the X-axis, normalized to the interval [-π, π].` Read-only attribute representing the angle of the vector in radians relative to the X-axis. This value is equivalent - to the `angle` attribute converted to radians and is normalized to the interval (-π, π]. + to the `angle` attribute converted to radians and is normalized to the interval [-π, π]. - Usage: Accessing `angle_rad` provides the current angle of the vector in radians within the specified range. + Usage: Accessing `angle_rad` provides the current angle of the vector in radians within the predefined range of [-π, π]. .. ## pygame.math.Vector2 ## diff --git a/src_c/doc/math_doc.h b/src_c/doc/math_doc.h index 75f06f4a5e..548465487d 100644 --- a/src_c/doc/math_doc.h +++ b/src_c/doc/math_doc.h @@ -40,8 +40,8 @@ #define DOC_MATH_VECTOR2_CLAMPMAGNITUDEIP "clamp_magnitude_ip(max_length, /) -> None\nclamp_magnitude_ip(min_length, max_length, /) -> None\nClamps the vector's magnitude between max_length and min_length" #define DOC_MATH_VECTOR2_UPDATE "update() -> None\nupdate(int) -> None\nupdate(float) -> None\nupdate(Vector2) -> None\nupdate(x, y) -> None\nupdate((x, y)) -> None\nSets the coordinates of the vector." #define DOC_MATH_VECTOR2_EPSILON "Determines the tolerance of vector calculations." -#define DOC_MATH_VECTOR2_ANGLE "Gives the angle of the vector in degrees, relative to the X-axis, normalized to the interval (-180, 180]." -#define DOC_MATH_VECTOR2_ANGLERAD "Gives the angle of the vector in radians, relative to the X-axis, normalized to the interval (-π, π]." +#define DOC_MATH_VECTOR2_ANGLE "Gives the angle of the vector in degrees, relative to the X-axis, normalized to the interval [-180, 180]." +#define DOC_MATH_VECTOR2_ANGLERAD "Gives the angle of the vector in radians, relative to the X-axis, normalized to the interval [-π, π]." #define DOC_MATH_VECTOR3 "Vector3() -> Vector3(0, 0, 0)\nVector3(int) -> Vector3\nVector3(float) -> Vector3\nVector3(Vector3) -> Vector3\nVector3(x, y, z) -> Vector3\nVector3((x, y, z)) -> Vector3\na 3-Dimensional Vector" #define DOC_MATH_VECTOR3_DOT "dot(Vector3, /) -> float\ncalculates the dot- or scalar-product with the other vector" #define DOC_MATH_VECTOR3_CROSS "cross(Vector3, /) -> Vector3\ncalculates the cross- or vector-product" diff --git a/src_c/math.c b/src_c/math.c index 4025b7bfc9..10238b6b6b 100644 --- a/src_c/math.c +++ b/src_c/math.c @@ -145,6 +145,9 @@ _vector_coords_from_string(PyObject *str, char **delimiter, double *coords, static void _vector_move_towards_helper(Py_ssize_t dim, double *origin_coords, double *target_coords, double max_distance); +static double +_pg_atan2(double y, double x); + /* generic vector functions */ static PyObject * pgVector_NEW(Py_ssize_t dim); @@ -637,6 +640,40 @@ vector_dealloc(pgVector *self) Py_TYPE(self)->tp_free((PyObject *)self); } +/* + *Returns rhe arctangent of the quotient y / x, in radians, considering the + *following special cases: atan2((anything), NaN ) is NaN; atan2(NAN , + *(anything) ) is NaN; atan2(+-0, +(anything but NaN)) is +-0 ; atan2(+-0, + *-(anything but NaN)) is +-pi ; atan2(+-(anything but 0 and NaN), 0) is + *+-pi/2; atan2(+-(anything but INF and NaN), +INF) is +-0 ; atan2(+-(anything + *but INF and NaN), -INF) is +-pi; atan2(+-INF,+INF ) is +-pi/4 ; + * atan2(+-INF,-INF ) is +-3pi/4; + * atan2(+-INF, (anything but,0,NaN, and INF)) is +-pi/2; + * + */ +static double +_pg_atan2(double y, double x) +{ + if (Py_IS_NAN(x) || Py_IS_NAN(y)) { + return Py_NAN; + } + + if (Py_IS_INFINITY(y)) { + if (Py_IS_INFINITY(x)) { + return copysign((copysign(1., x) == 1.) ? 0.25 * Py_MATH_PI + : 0.75 * Py_MATH_PI, + y); + } + return copysign(0.5 * Py_MATH_PI, y); + } + + if (Py_IS_INFINITY(x) || y == 0.) { + return copysign((copysign(1., x) == 1.) ? 0. : Py_MATH_PI, y); + } + + return atan2(y, x); +} + /********************************************** * Generic vector PyNumber emulation routines **********************************************/ @@ -1276,51 +1313,20 @@ vector_setz(pgVector *self, PyObject *value, void *closure) } static PyObject * -vector_get_angle(pgVector *self, void *closure) +vector_get_angle_rad(pgVector *self, void *closure) { - PyObject *angle_obj = vector_get_angle_rad(self, closure); - double angle_rad = PyFloat_AsDouble(angle_obj); - double angle_deg = angle_rad * RAD_TO_DEG; - - if (angle_deg > 180.0) { - angle_deg -= 360.0; - } - else if (angle_deg <= -180.0) { - angle_deg += 360.0; - } + double angle_rad = _pg_atan2(self->coords[1], self->coords[0]); - return PyFloat_FromDouble(angle_deg); + return PyFloat_FromDouble(angle_rad); } static PyObject * -vector_get_angle_rad(pgVector *self, void *closure) +vector_get_angle(pgVector *self, void *closure) { - pgVector *vec = self; - double x = vec->coords[0]; - double y = vec->coords[1]; - - if (Py_IS_NAN(x) || Py_IS_NAN(y)) { - return PyFloat_FromDouble(Py_NAN); - } - - if (Py_IS_INFINITY(y)) { - if (Py_IS_INFINITY(x)) { - if (copysign(1., x) == 1.) - return PyFloat_FromDouble(copysign(0.25 * Py_MATH_PI, y)); - else - return PyFloat_FromDouble(copysign(0.75 * Py_MATH_PI, y)); - } - return PyFloat_FromDouble(copysign(0.5 * Py_MATH_PI, y)); - } - - if (Py_IS_INFINITY(x) || y == 0.) { - if (copysign(1., x) == 1.) - return PyFloat_FromDouble(copysign(0., y)); - else - return PyFloat_FromDouble(copysign(Py_MATH_PI, y)); - } + double angle_rad = _pg_atan2(self->coords[1], self->coords[0]); + double angle_deg = angle_rad * RAD_TO_DEG; - return PyFloat_FromDouble(atan2(y, x)); + return PyFloat_FromDouble(angle_deg); } static PyObject * From f7f6961987d612dc78d6244156be1c3bf26d3ee7 Mon Sep 17 00:00:00 2001 From: AntoineMamou Date: Thu, 5 Dec 2024 15:05:16 +0100 Subject: [PATCH 11/11] Add loop and fade options to pygame.mixer.channel.queue() --- docs/reST/ref/mixer.rst | 9 +++++++ src_c/mixer.c | 55 +++++++++++++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/docs/reST/ref/mixer.rst b/docs/reST/ref/mixer.rst index 5a441c7f81..2f4714006e 100644 --- a/docs/reST/ref/mixer.rst +++ b/docs/reST/ref/mixer.rst @@ -677,6 +677,15 @@ The following file formats are supported If there is no sound actively playing on the Channel then the Sound will begin playing immediately. + The optional parameters ``loops`` and ``fade_ms`` control how the queued + Sound is played: + + - ``loops``: The number of times the queued Sound will loop. A value of 0 + means the sound will play once, 1 means it will play twice, and so on. + - ``fade_ms``: The duration of the fade-in effect in milliseconds when the + queued Sound starts playing. If set to 0 (default), the Sound will play + without a fade. + .. ## Channel.queue ## .. method:: get_queue diff --git a/src_c/mixer.c b/src_c/mixer.c index f342e18216..25998d6fa7 100644 --- a/src_c/mixer.c +++ b/src_c/mixer.c @@ -104,6 +104,8 @@ static char *request_devicename = NULL; struct ChannelData { PyObject *sound; PyObject *queue; + int queue_loop; + int queue_fade_ms; int endevent; }; static struct ChannelData *channeldata = NULL; @@ -294,15 +296,35 @@ endsound_callback(int channel) if (channeldata[channel].queue) { PyGILState_STATE gstate = PyGILState_Ensure(); - int channelnum; Mix_Chunk *sound = pgSound_AsChunk(channeldata[channel].queue); + + // Récupérer les paramètres de la queue + int queue_loop = channeldata[channel].queue_loop; + int queue_fade_ms = channeldata[channel].queue_fade_ms; + + // Libérer le son actuel et déplacer le son de la queue dans + // "sound" Py_XDECREF(channeldata[channel].sound); channeldata[channel].sound = channeldata[channel].queue; channeldata[channel].queue = NULL; - PyGILState_Release(gstate); - channelnum = Mix_PlayChannelTimed(channel, sound, 0, -1); - if (channelnum != -1) + + // Jouer le son avec les paramètres de fade-in ou directement + int channelnum; + if (queue_fade_ms > 0) { + channelnum = Mix_FadeInChannelTimed(channel, sound, queue_loop, + queue_fade_ms, -1); + } + else { + channelnum = + Mix_PlayChannelTimed(channel, sound, queue_loop, -1); + } + + // Associer le canal au groupe + if (channelnum != -1) { Mix_GroupChannel(channelnum, (int)(intptr_t)sound); + } + + PyGILState_Release(gstate); } else { PyGILState_STATE gstate = PyGILState_Ensure(); @@ -1074,22 +1096,35 @@ chan_get_id(PyObject *self, PyObject *empty_args) } static PyObject * -chan_queue(PyObject *self, PyObject *sound) +chan_queue(PyObject *self, PyObject *args, PyObject *kwargs) { int channelnum = pgChannel_AsInt(self); Mix_Chunk *chunk; + PyObject *sound; + int loop = 0; + int fade_ms = 0; + + static char *kwlist[] = {"sound", "loop", "fade_ms", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|ii", kwlist, &sound, + &loop, &fade_ms)) { + return RAISE(PyExc_TypeError, + "Expected arguments: sound, optional int loop, optional " + "int fade_ms"); + } if (!pgSound_Check(sound)) { return RAISE(PyExc_TypeError, - "The argument must be an instance of Sound"); + "The first argument must be an instance of Sound"); } chunk = pgSound_AsChunk(sound); CHECK_CHUNK_VALID(chunk, NULL); + if (!channeldata[channelnum].sound) /*nothing playing*/ { Py_BEGIN_ALLOW_THREADS; - channelnum = Mix_PlayChannelTimed(channelnum, chunk, 0, -1); + channelnum = Mix_PlayChannelTimed(channelnum, chunk, loop, -1); if (channelnum != -1) Mix_GroupChannel(channelnum, (int)(intptr_t)chunk); Py_END_ALLOW_THREADS; @@ -1100,8 +1135,11 @@ chan_queue(PyObject *self, PyObject *sound) else { Py_XDECREF(channeldata[channelnum].queue); channeldata[channelnum].queue = sound; + channeldata[channelnum].queue_loop = loop; + channeldata[channelnum].queue_fade_ms = fade_ms; Py_INCREF(sound); } + Py_RETURN_NONE; } @@ -1321,7 +1359,8 @@ static PyGetSetDef _channel_getsets[] = { static PyMethodDef channel_methods[] = { {"play", (PyCFunction)chan_play, METH_VARARGS | METH_KEYWORDS, DOC_MIXER_CHANNEL_PLAY}, - {"queue", chan_queue, METH_O, DOC_MIXER_CHANNEL_QUEUE}, + {"queue", (PyCFunction)chan_queue, METH_VARARGS | METH_KEYWORDS, + DOC_MIXER_CHANNEL_QUEUE}, {"get_busy", (PyCFunction)chan_get_busy, METH_NOARGS, DOC_MIXER_CHANNEL_GETBUSY}, {"fadeout", chan_fadeout, METH_VARARGS, DOC_MIXER_CHANNEL_FADEOUT},