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):