Skip to content

Commit

Permalink
Add 3D rotation around arbitrary point
Browse files Browse the repository at this point in the history
  • Loading branch information
samirelanduk committed Mar 31, 2018
1 parent c91d551 commit 5d7967c
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 106 deletions.
23 changes: 7 additions & 16 deletions points/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def rotate_2d_vectors(angle, *vectors, point=None):

@round_vectors
@allow_degrees
def rotate_3d_vectors(angle, dimension, *vectors):
def rotate_3d_vectors(angle, dimension, *vectors, point=None):
"""Rotates 3 dimensional vectors.
:param float angle: The angle in radians.
Expand All @@ -101,6 +101,11 @@ def rotate_3d_vectors(angle, dimension, *vectors):
raise TypeError("Cannot rotate {} - not a vector".format(v))
if len(v._values) != 3:
raise ValueError("Cannot rotate {} - not 3D".format(v))
if point:
if len(point) != 3:
raise ValueError("point {} is not 3D".format(point))
dx, dy, dz = point
translate_vectors((-dx, -dy, -dz), *vectors)
if dimension == 0:
matrix = Matrix(
[1, 0, 0], [0, cos(angle), -sin(angle)], [0, sin(angle), cos(angle)]
Expand All @@ -117,18 +122,4 @@ def rotate_3d_vectors(angle, dimension, *vectors):
raise ValueError("{} is not a valid dimensions".format(dimension))
for vector in vectors:
vector._values = (matrix @ vector)._values



'''def degree_angle(func):
"""A decorator which takes a function that returns an angle in radians and
confers upon it the ability to return it in degrees."""
def new_func(*args, degrees=False, **kwargs):
angle = func(*args, **kwargs)
if degrees:
return math.degrees(angle)
return angle
new_func.__name__ = func.__name__
new_func.__doc__ = func.__doc__
return new_func'''
if point: translate_vectors((dx, dy, dz), *vectors)
84 changes: 10 additions & 74 deletions points/vectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
from math import sqrt, acos

class Vector:
"""A vector is a sequence of numbers. They can represent a point in space,
or the attributes of an object.
Vectors can be added and subtracted with ``+`` and ``-``, but ``*`` is
reserved for scalar multiplication - you can use it to multiply the vector
by a number but not by another vector (there are special methods for this).
:param values: The numbers that make up the Vector. If a single sequence is\
given, that sequence will be unpacked to make the vector.
:raises TypeError: if the values given are not numeric."""

def __init__(self, *values):
if len(values) == 1:
Expand Down Expand Up @@ -318,77 +328,3 @@ def rank(self):
:rtype: ``int``"""

return len(self._vectors)
'''
, sin, cos, acos, degrees
from .geometry import degree_angle
class Vector:
"""A vector is a sequence of numbers. They can represent a point in space,
or the attributes of an object.
Vectors can be added and subtracted with ``+`` and ``-``, but ``*`` is
reserved for scalar multiplication - you can use it to multiply the vector
by a number but not by another vector (there are special methods for this).
:param values: The numbers that make up the Vector. If a single sequence is\
given, that sequence will be unpacked to make the vector.
:raises TypeError: if the values given are not numeric."""
def rotate(self, angle, axis="x"):
"""A 2 dimensional vector can be rotated by supplying an angle in
radians. A 3 dimensional vector can be rotated by supplying an angle in
radians and an axis to rotate around (``"x"``, ``"y"`` or ``"z"``).
All rotation is right handed. No vectors of other dimensions can
currently be rotated.
:param float angle: the angle to rotate by.
:param str axis: the axis to rotate around if in 3D.
:raises ValueError: if the vector length is not 2 or 3.
:raises ValueError: if axis given is invalid."""
from .matrices import Matrix
matrix = None
if len(self._values) == 2:
matrix = Matrix([cos(angle), -sin(angle)], [sin(angle), cos(angle)])
elif len(self._values) == 3:
if axis == "x":
matrix = Matrix(
[1, 0, 0],
[0, cos(angle), -sin(angle)],
[0, sin(angle), cos(angle)]
)
elif axis == "y":
matrix = Matrix(
[cos(angle), 0, sin(angle)],
[0, 1, 0],
[-sin(angle), 0, cos(angle)]
)
elif axis == "z":
matrix = Matrix(
[cos(angle), -sin(angle), 0],
[sin(angle), cos(angle), 0],
[0, 0, 1]
)
else:
raise ValueError("Axis '{}' is not z, y or z".format(axis))
else:
raise ValueError("Only 2 and 3 dimensional vectors can rotate")
self._values = (matrix @ self)._values
@degree_angle
'''
9 changes: 9 additions & 0 deletions tests/integration/test_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,12 @@ def test_3d_transformations(self):
self.assertEqual(v2.values(), (-125.30, -716.84, -699.11))
self.assertEqual(v3.values(), (-215.31, -719.21, -668.40))
self.assertEqual(v4.values(), (-101.65, -709.84, -696.77))

# Rotation around arbitrary point
v5 = points.Vector(2, 2, 2)
v6 = points.Vector(-1, -1, -1)
points.rotate_3d_vectors(
90, 0, v5, v6, degrees=True, trim=6, point=[1, 1, 1]
)
self.assertEqual(v5.values(), (2, 0, 2))
self.assertEqual(v6.values(), (-1, 3, -1))
73 changes: 57 additions & 16 deletions tests/unit/test_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,15 +148,18 @@ class Rotation3dTests(GeometryTest):
def setUp(self):
GeometryTest.setUp(self)
self.patch1 = patch("points.geometry.Matrix")
self.patch2 = patch("points.geometry.translate_vectors")
self.mock_trans = self.patch2.start()
self.mock_matrix = self.patch1.start()
self.v3._values.pop()


def tearDown(self):
self.patch1.stop()
self.patch2.stop()


def test_can_rotate_3d(self):
def test_can_rotate_3d_x(self):
matrix = Mock()
self.mock_matrix.return_value = matrix
matrix.__matmul__ = MagicMock()
Expand All @@ -169,6 +172,56 @@ def test_can_rotate_3d(self):
matrix.__matmul__.assert_any_call(self.v2)
self.assertEqual(self.v1._values, [4, 5, 6])
self.assertEqual(self.v2._values, [7, 8])
self.assertFalse(self.mock_trans.called)


def test_can_rotate_3d_y(self):
matrix = Mock()
self.mock_matrix.return_value = matrix
matrix.__matmul__ = MagicMock()
matrix.__matmul__.side_effect = [self.v2, self.v3, self.v1]
rotate_3d_vectors(0.5, 1, self.v1, self.v2)
self.mock_matrix.assert_called_with(
[cos(0.5), 0, sin(0.5)], [0, 1, 0], [-sin(0.5), 0, cos(0.5)]
)
matrix.__matmul__.assert_any_call(self.v1)
matrix.__matmul__.assert_any_call(self.v2)
self.assertEqual(self.v1._values, [4, 5, 6])
self.assertEqual(self.v2._values, [7, 8])
self.assertFalse(self.mock_trans.called)


def test_can_rotate_3d_z(self):
matrix = Mock()
self.mock_matrix.return_value = matrix
matrix.__matmul__ = MagicMock()
matrix.__matmul__.side_effect = [self.v2, self.v3, self.v1]
rotate_3d_vectors(0.5, 2, self.v1, self.v2)
self.mock_matrix.assert_called_with(
[cos(0.5), -sin(0.5), 0], [sin(0.5), cos(0.5), 0], [0, 0, 1]
)
matrix.__matmul__.assert_any_call(self.v1)
matrix.__matmul__.assert_any_call(self.v2)
self.assertEqual(self.v1._values, [4, 5, 6])
self.assertEqual(self.v2._values, [7, 8])
self.assertFalse(self.mock_trans.called)


def test_can_rotate_3d_about_point(self):
matrix = Mock()
self.mock_matrix.return_value = matrix
matrix.__matmul__ = MagicMock()
matrix.__matmul__.side_effect = [self.v2, self.v3, self.v1]
rotate_3d_vectors(0.5, 0, self.v1, self.v2, point=[1, 2, 3])
self.mock_trans.assert_any_call((-1, -2, -3), self.v1, self.v2)
self.mock_matrix.assert_called_with(
[1, 0, 0], [0, cos(0.5), -sin(0.5)], [0, sin(0.5), cos(0.5)]
)
matrix.__matmul__.assert_any_call(self.v1)
matrix.__matmul__.assert_any_call(self.v2)
self.assertEqual(self.v1._values, [4, 5, 6])
self.assertEqual(self.v2._values, [7, 8])
self.mock_trans.assert_any_call((1, 2, 3), self.v1, self.v2)


def test_rotation_needs_vectors(self):
Expand All @@ -186,18 +239,6 @@ def test_dimension_must_be_valid(self):
rotate_3d_vectors(0.5, 3, self.v1, self.v2)



'''
from math import pi
from points.geometry import degree_angle
class AngleDecoratorTests(TestCase):
def test_decorator_can_convert_to_degrees(self):
def angle():
return pi
angle = degree_angle(angle)
self.assertEqual(angle(), pi)
self.assertEqual(angle(degrees=False), pi)
self.assertEqual(angle(degrees=True), 180)
'''
def test_point_must_be_correct_dimension(self):
with self.assertRaises(ValueError):
rotate_3d_vectors(0.5, 1, self.v1, self.v2, point=[1, 2])

0 comments on commit 5d7967c

Please sign in to comment.