Skip to content

Commit b0fa4ab

Browse files
AntoineMamouGabrieleGabrieleLSaatleoddbookworm
authored
new angle and angle_rad property in vector2 (#3222)
* Changes to be commited: new file: test.txt * Delete test.txt * add angle and angle_rad property for vector2 * syntax error correction * code modification for properties angle and angle rad and new doc for these properties * type stubs correction * add new property vector2_default_angle * correction math_test.py * .angle and .angle_rad with math.atan2 behaviour for special cases * adding helper function _pg_atan2 + docs modification * Add versionadded doc to angle properties * Update version to 2.5.5 --------- Co-authored-by: Gabriele <[email protected]> Co-authored-by: GabrieleLS <[email protected]> Co-authored-by: aatle <[email protected]> Co-authored-by: Andrew Coffey <[email protected]>
1 parent 885b64f commit b0fa4ab

File tree

5 files changed

+188
-0
lines changed

5 files changed

+188
-0
lines changed

buildconfig/stubs/pygame/math.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ class Vector2(_GenericVector):
209209
xy: Vector2
210210
yx: Vector2
211211
yy: Vector2
212+
@property
213+
def angle(self) -> float: ...
214+
@property
215+
def angle_rad(self) -> float: ...
212216
@overload
213217
def __init__(
214218
self: _TVec,

docs/reST/ref/math.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,27 @@ Multiple coordinates can be set using slices or swizzling
616616
find that either the margin is too large or too small, in which case changing ``epsilon`` slightly
617617
might help you out.
618618

619+
.. attribute:: angle
620+
621+
| :sl:`Gives the angle of the vector in degrees, relative to the X-axis, normalized to the interval [-180, 180].`
622+
623+
Read-only attribute representing the angle of the vector in degrees relative to the X-axis. This angle is normalized to
624+
the interval [-180, 180].
625+
626+
Usage: Accessing `angle` provides the current angle of the vector in degrees within the predefined range of [-180, 180].
627+
628+
.. versionadded:: 2.5.5
629+
630+
.. attribute:: angle_rad
631+
632+
| :sl:`Gives the angle of the vector in radians, relative to the X-axis, normalized to the interval [-π, π].`
633+
634+
Read-only attribute representing the angle of the vector in radians relative to the X-axis. This value is equivalent
635+
to the `angle` attribute converted to radians and is normalized to the interval [-π, π].
636+
637+
Usage: Accessing `angle_rad` provides the current angle of the vector in radians within the predefined range of [-π, π].
638+
639+
.. versionadded:: 2.5.5
619640

620641
.. ## pygame.math.Vector2 ##
621642

src_c/doc/math_doc.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
#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"
4141
#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."
4242
#define DOC_MATH_VECTOR2_EPSILON "Determines the tolerance of vector calculations."
43+
#define DOC_MATH_VECTOR2_ANGLE "Gives the angle of the vector in degrees, relative to the X-axis, normalized to the interval [-180, 180]."
44+
#define DOC_MATH_VECTOR2_ANGLERAD "Gives the angle of the vector in radians, relative to the X-axis, normalized to the interval [-π, π]."
4345
#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"
4446
#define DOC_MATH_VECTOR3_DOT "dot(Vector3, /) -> float\ncalculates the dot- or scalar-product with the other vector"
4547
#define DOC_MATH_VECTOR3_CROSS "cross(Vector3, /) -> Vector3\ncalculates the cross- or vector-product"

src_c/math.c

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151

5252
#define TWO_PI (2. * M_PI)
5353

54+
#define RAD_TO_DEG (180.0 / M_PI)
55+
#define DEG_TO_RAD (M_PI / 180.0)
56+
5457
#ifndef M_PI_2
5558
#define M_PI_2 (M_PI / 2.0)
5659
#endif /* M_PI_2 */
@@ -142,6 +145,8 @@ _vector_coords_from_string(PyObject *str, char **delimiter, double *coords,
142145
static void
143146
_vector_move_towards_helper(Py_ssize_t dim, double *origin_coords,
144147
double *target_coords, double max_distance);
148+
static double
149+
_pg_atan2(double y, double x);
145150

146151
/* generic vector functions */
147152
static PyObject *
@@ -202,6 +207,10 @@ vector_sety(pgVector *self, PyObject *value, void *closure);
202207
static int
203208
vector_setz(pgVector *self, PyObject *value, void *closure);
204209
static PyObject *
210+
vector_get_angle(pgVector *self, void *closure);
211+
static PyObject *
212+
vector_get_angle_rad(pgVector *self, void *closure);
213+
static PyObject *
205214
vector_richcompare(PyObject *o1, PyObject *o2, int op);
206215
static PyObject *
207216
vector_length(pgVector *self, PyObject *args);
@@ -635,6 +644,40 @@ vector_dealloc(pgVector *self)
635644
Py_TYPE(self)->tp_free((PyObject *)self);
636645
}
637646

647+
/*
648+
*Returns rhe arctangent of the quotient y / x, in radians, considering the
649+
*following special cases: atan2((anything), NaN ) is NaN; atan2(NAN ,
650+
*(anything) ) is NaN; atan2(+-0, +(anything but NaN)) is +-0 ; atan2(+-0,
651+
*-(anything but NaN)) is +-pi ; atan2(+-(anything but 0 and NaN), 0) is
652+
*+-pi/2; atan2(+-(anything but INF and NaN), +INF) is +-0 ; atan2(+-(anything
653+
*but INF and NaN), -INF) is +-pi; atan2(+-INF,+INF ) is +-pi/4 ;
654+
* atan2(+-INF,-INF ) is +-3pi/4;
655+
* atan2(+-INF, (anything but,0,NaN, and INF)) is +-pi/2;
656+
*
657+
*/
658+
static double
659+
_pg_atan2(double y, double x)
660+
{
661+
if (Py_IS_NAN(x) || Py_IS_NAN(y)) {
662+
return Py_NAN;
663+
}
664+
665+
if (Py_IS_INFINITY(y)) {
666+
if (Py_IS_INFINITY(x)) {
667+
return copysign((copysign(1., x) == 1.) ? 0.25 * Py_MATH_PI
668+
: 0.75 * Py_MATH_PI,
669+
y);
670+
}
671+
return copysign(0.5 * Py_MATH_PI, y);
672+
}
673+
674+
if (Py_IS_INFINITY(x) || y == 0.) {
675+
return copysign((copysign(1., x) == 1.) ? 0. : Py_MATH_PI, y);
676+
}
677+
678+
return atan2(y, x);
679+
}
680+
638681
/**********************************************
639682
* Generic vector PyNumber emulation routines
640683
**********************************************/
@@ -1303,6 +1346,23 @@ vector_setz(pgVector *self, PyObject *value, void *closure)
13031346
return vector_set_component(self, value, 2);
13041347
}
13051348

1349+
static PyObject *
1350+
vector_get_angle_rad(pgVector *self, void *closure)
1351+
{
1352+
double angle_rad = _pg_atan2(self->coords[1], self->coords[0]);
1353+
1354+
return PyFloat_FromDouble(angle_rad);
1355+
}
1356+
1357+
static PyObject *
1358+
vector_get_angle(pgVector *self, void *closure)
1359+
{
1360+
double angle_rad = _pg_atan2(self->coords[1], self->coords[0]);
1361+
double angle_deg = angle_rad * RAD_TO_DEG;
1362+
1363+
return PyFloat_FromDouble(angle_deg);
1364+
}
1365+
13061366
static PyObject *
13071367
vector_richcompare(PyObject *o1, PyObject *o2, int op)
13081368
{
@@ -2675,6 +2735,9 @@ static PyMethodDef vector2_methods[] = {
26752735
static PyGetSetDef vector2_getsets[] = {
26762736
{"x", (getter)vector_getx, (setter)vector_setx, NULL, NULL},
26772737
{"y", (getter)vector_gety, (setter)vector_sety, NULL, NULL},
2738+
{"angle", (getter)vector_get_angle, NULL, DOC_MATH_VECTOR2_ANGLE, NULL},
2739+
{"angle_rad", (getter)vector_get_angle_rad, NULL,
2740+
DOC_MATH_VECTOR2_ANGLERAD, NULL},
26782741
{NULL, 0, NULL, NULL, NULL} /* Sentinel */
26792742
};
26802743

test/math_test.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,104 @@ def test_del_y(self):
14101410
exception = ctx.exception
14111411
self.assertEqual(str(exception), "Cannot delete the y attribute")
14121412

1413+
def test_angle_rad_property(self):
1414+
v0 = Vector2(1, 0)
1415+
self.assertEqual(v0.angle_rad, 0.0)
1416+
1417+
v1 = Vector2(0, 1)
1418+
self.assertEqual(v1.angle_rad, math.pi / 2)
1419+
1420+
v2 = Vector2(-1, 0)
1421+
self.assertEqual(v2.angle_rad, math.pi)
1422+
1423+
v3 = Vector2(0, -1)
1424+
self.assertEqual(v3.angle_rad, -math.pi / 2)
1425+
1426+
v4 = Vector2(1, 1)
1427+
self.assertEqual(v4.angle_rad, math.pi / 4)
1428+
1429+
v5 = Vector2(-1, 1)
1430+
self.assertEqual(v5.angle_rad, 3 * math.pi / 4)
1431+
1432+
v6 = Vector2(-1, -1)
1433+
self.assertEqual(v6.angle_rad, -3 * math.pi / 4)
1434+
1435+
v7 = Vector2(1, -1)
1436+
self.assertEqual(v7.angle_rad, -math.pi / 4)
1437+
1438+
v8 = Vector2(float('inf'), float('inf'))
1439+
self.assertEqual(v8.angle_rad, math.pi / 4)
1440+
1441+
v9 = Vector2(float('-inf'), float('inf'))
1442+
self.assertEqual(v9.angle_rad, 3 * math.pi / 4)
1443+
1444+
v10 = Vector2(float('-inf'), float('-inf'))
1445+
self.assertEqual(v10.angle_rad, -3 * math.pi / 4)
1446+
1447+
v11 = Vector2(float('inf'), float('-inf'))
1448+
self.assertEqual(v11.angle_rad, -math.pi / 4)
1449+
1450+
v12 = Vector2(0, 0)
1451+
self.assertEqual(v12.angle_rad, 0.0)
1452+
1453+
v13 = Vector2(float('nan'), 1)
1454+
self.assertTrue(math.isnan(v13.angle_rad))
1455+
1456+
v14 = Vector2(1, float('nan'))
1457+
self.assertTrue(math.isnan(v14.angle_rad))
1458+
1459+
v15 = Vector2(float('nan'), float('nan'))
1460+
self.assertTrue(math.isnan(v15.angle_rad))
1461+
1462+
def test_angle_property(self):
1463+
v0 = pygame.math.Vector2(1, 0)
1464+
self.assertEqual(v0.angle, 0.0)
1465+
1466+
v1 = pygame.math.Vector2(0, 1)
1467+
self.assertEqual(v1.angle, 90.0)
1468+
1469+
v2 = pygame.math.Vector2(-1, 0)
1470+
self.assertEqual(v2.angle, 180.0)
1471+
1472+
v3 = pygame.math.Vector2(0, -1)
1473+
self.assertEqual(v3.angle, -90.0)
1474+
1475+
v4 = pygame.math.Vector2(1, 1)
1476+
self.assertEqual(v4.angle, 45.0)
1477+
1478+
v5 = pygame.math.Vector2(-1, 1)
1479+
self.assertEqual(v5.angle, 135.0)
1480+
1481+
v6 = pygame.math.Vector2(-1, -1)
1482+
self.assertEqual(v6.angle, -135.0)
1483+
1484+
v7 = pygame.math.Vector2(1, -1)
1485+
self.assertEqual(v7.angle, -45.0)
1486+
1487+
v8 = pygame.math.Vector2(float('inf'), float('inf'))
1488+
self.assertEqual(v8.angle, 45.0)
1489+
1490+
v9 = pygame.math.Vector2(float('-inf'), float('inf'))
1491+
self.assertEqual(v9.angle, 135.0)
1492+
1493+
v10 = pygame.math.Vector2(float('-inf'), float('-inf'))
1494+
self.assertEqual(v10.angle, -135.0)
1495+
1496+
v11 = pygame.math.Vector2(float('inf'), float('-inf'))
1497+
self.assertEqual(v11.angle, -45.0)
1498+
1499+
v12 = pygame.math.Vector2(0, 0)
1500+
self.assertEqual(v12.angle, 0.0)
1501+
1502+
v13 = pygame.math.Vector2(float('nan'), 1)
1503+
self.assertTrue(math.isnan(v13.angle))
1504+
1505+
v14 = pygame.math.Vector2(1, float('nan'))
1506+
self.assertTrue(math.isnan(v14.angle))
1507+
1508+
v15 = pygame.math.Vector2(float('nan'), float('nan'))
1509+
self.assertTrue(math.isnan(v15.angle))
1510+
14131511

14141512
class Vector3TypeTest(unittest.TestCase):
14151513
def setUp(self):

0 commit comments

Comments
 (0)