Skip to content

Commit

Permalink
.angle and .angle_rad with math.atan2 behaviour for special cases
Browse files Browse the repository at this point in the history
  • Loading branch information
AntoineMamou committed Nov 18, 2024
1 parent ad6f04f commit 71c12a7
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 114 deletions.
1 change: 0 additions & 1 deletion buildconfig/stubs/pygame/math.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@ class Vector2(_GenericVector):
yy: Vector2
angle: float
angle_rad: float
vector2_default_angle : float
@overload
def __init__(
self: _TVec,
Expand Down
15 changes: 2 additions & 13 deletions docs/reST/ref/math.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand Down
1 change: 0 additions & 1 deletion src_c/doc/math_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
83 changes: 28 additions & 55 deletions src_c/math.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 *
Expand Down Expand Up @@ -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 */
};

Expand Down Expand Up @@ -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},
Expand Down
124 changes: 80 additions & 44 deletions test/math_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit 71c12a7

Please sign in to comment.