diff --git a/README.md b/README.md index 29cd5f1e..d1522d3f 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,14 @@ Multiply a `Vector2` by another `Vector2` to get the dot product. >>> Vector2(45, 60).length 75.0 +### Cross-product + +Take the cross-product between two (2D) vectors. +The result is expressed as a scalar, as it is known to lie on the z-axis. + + >>> Vector(1, 0) ^ Vector(0, 1) + 1 + ### Access Values Convenient access to `Vector2` members via dot notation, indexes, or keys. @@ -122,6 +130,16 @@ Rotate a vector in relation to its own origin and return a new `Vector2`. Positive rotation is counter/anti-clockwise. +#### angle(vector) + +Compute the angle between two vectors, expressed as a scalar in degrees. + + >>> Vector(1, 0).angle(Vector(0, 1)) + 90 + +As with `rotate()`, angles are signed, and refer to a direct coordinate system +(i.e. positive rotations are counter-clockwise). + #### normalize() Return the normalized `Vector2` for the given `Vector2`. diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index e13eda4d..04ea9d75 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -1,4 +1,4 @@ -from math import cos, hypot, radians, sin +from math import acos, cos, degrees, hypot, radians, sin from numbers import Number from collections.abc import Sequence @@ -46,6 +46,9 @@ def __rmul__(self, other): if isinstance(other, Number): return Vector2(self.x * other, self.y * other) + def __xor__(self, other): + return self.x * other.y - self.y * other.x + def __getitem__(self, item): if hasattr(item, '__index__'): item = item.__index__() @@ -88,6 +91,9 @@ def __iter__(self): def __neg__(self): return self * -1 + def angle(self, other): + return degrees(acos(self.normalize() * other.normalize())) + def rotate(self, degrees): r = radians(degrees) r_cos = cos(r) diff --git a/tests/test_vector2_rotate.py b/tests/test_vector2_rotate.py index 4fe3d4d8..30142840 100644 --- a/tests/test_vector2_rotate.py +++ b/tests/test_vector2_rotate.py @@ -11,9 +11,14 @@ (Vector2(math.pi, math.e), 67, Vector2(-1.27467, 3.95397)) ] +def isclose(x, y, epsilon = 6.5e-5): + d = (x - y) % 360 + return (d < epsilon) or (d > 360 - epsilon) + @pytest.mark.parametrize('input, degrees, expected', data) def test_multiple_rotations(input, degrees, expected): assert input.rotate(degrees) == expected + assert isclose(input.angle(expected), degrees) def test_for_exception(): with pytest.raises(TypeError):