import math
import operator
from pint.unit import _Unit
from .. import generic
from ..geometry import AbstractPolygon, Geometry, tau, valid_scalar
from .util import (
Spatial,
_connect_point3_line3,
_connect_point3_sphere,
_connect_point3_plane,
_intersect_point3_sphere
)
def operate(op, self, other):
if valid_scalar(other):
return self.__class__(op(self.x, other), op(self.y, other), op(self.z, other))
[docs]class Vector3(generic.Concrete, generic.Vector, Spatial):
"""
A three-dimensional vector supporting all corresponding built-in math
operators:
>>> Vector(1, 2, 3) + Vector(2, 2, 2)
Vector(3, 4, 5)
>>> Vector(1, 2, 3) - Vector(2, 2, 2)
Vector(-1, 0, 1)
>>> Vector(1, 0, 1) * 5
Vector(5, 0, 5)
>>> Vector(1, 0, 1) / 5
Vector(0.2, 0.0, 0.2)
>>> Vector(1, 1, 1) == Vector(1, 1, 1)
True
In addition to many other specialized vector operations.
Defines convenience `.basis` members for commonly used basis vectors:
>>> Vector.basis.x; Vector.bx
Vector(1, 0, 0)
Vector(1, 0, 0)
>>> Vector.basis.y; Vector.by
Vector(0, 1, 0)
Vector(0, 1, 0)
>>> Vector.basis.z; Vector.bz
Vector(0, 0, 1)
Vector(0, 0, 1)
"""
__slots__ = ['x', 'y', 'z']
def __init__(self, x=0, y=0, z=0):
self.x = x
self.y = y
self.z = z
def __copy__(self):
return self.__class__(self.x, self.y, self.z)
copy = __copy__
def __repr__(self):
return 'Vector({0!r}, {1!r}, {2!r})'.format(*self.xyz)
def __hash__(self):
return hash((self.x, self.y, self.z))
def __eq__(self, other):
if isinstance(other, Vector3):
return self.__class__ == other.__class__ and \
self.x == other.x and \
self.y == other.y and \
self.z == other.z
else:
assert hasattr(other, '__len__') and len(other) == 3
return self.x == other[0] and \
self.y == other[1] and \
self.z == other[2]
def __ne__(self, other):
return not self.__eq__(other)
def __nonzero__(self):
return bool(self.x != 0 or self.y != 0 or self.z != 0)
def __len__(self):
return 3
def __getitem__(self, key):
return (self.x, self.y, self.z)[key]
def __setitem__(self, key, value):
l = [self.x, self.y, self.z]
l[key] = value
self.x, self.y, self.z = l
def __iter__(self):
return iter((self.x, self.y, self.z))
def __getattr__(self, name):
try:
return tuple([(self.x, self.y, self.z)['xyz'.index(c)] \
for c in name])
except ValueError:
raise AttributeError(name)
def __add__(self, other):
if isinstance(other, Vector3):
# Vector + Vector -> Vector
# Vector + Point -> Point
# Point + Point -> Vector
if self.__class__ is other.__class__:
_class = Vector3
else:
_class = Point3
return _class(self.x + other.x,
self.y + other.y,
self.z + other.z)
else:
assert hasattr(other, '__len__') and len(other) == 3
return self.__class__(self.x + other[0],
self.y + other[1],
self.z + other[2])
__radd__ = __add__
def __iadd__(self, other):
if isinstance(other, Vector3):
self.x += other.x
self.y += other.y
self.z += other.z
else:
self.x += other[0]
self.y += other[1]
self.z += other[2]
return self
def __sub__(self, other):
if isinstance(other, Vector3):
# Vector - Vector -> Vector
# Vector - Point -> Point
# Point - Point -> Vector
if self.__class__ is other.__class__:
_class = Vector3
else:
_class = Point3
return _class(self.x - other.x,
self.y - other.y,
self.z - other.z)
else:
assert hasattr(other, '__len__') and len(other) == 3
return self.__class__(self.x - other[0],
self.y - other[1],
self.z - other[2])
def __mul__(self, other):
if isinstance(other, _Unit):
assert (1 * other).check('[length]'), 'only compatible with length units'
return NotImplemented
elif valid_scalar(other):
return self.__class__(self.x * other,
self.y * other,
self.z * other)
else:
return NotImplemented
__rmul__ = __mul__
def __floordiv__(self, other):
return operate(operator.floordiv, self, other)
def __rfloordiv__(self, other):
return operate(operator.floordiv, other, self)
def __truediv__(self, other):
return operate(operator.truediv, self, other)
def __rtruediv__(self, other):
return operate(operator.truediv, other, self)
def __neg__(self):
return self.__class__(-self.x, -self.y, -self.z)
__pos__ = __copy__
def __abs__(self):
return math.sqrt(self.x ** 2 + \
self.y ** 2 + \
self.z ** 2)
magnitude = __abs__
def magnitude_squared(self):
return self.x ** 2 + \
self.y ** 2 + \
self.z ** 2
def normalize(self):
d = self.magnitude()
if d:
self.x /= d
self.y /= d
self.z /= d
return self
[docs] def normalized(self):
"""
Returns a vector with the same direction but unit (1) length:
>>> Vector(0, 0, 5).normalized()
Vector(0.0, 0.0, 1.0)
"""
return self / self.magnitude()
[docs] def rounded(self, place=None):
"""
Rounds all elements to `place` decimals.
"""
return self.__class__(*(round(v, place) for v in self.xyz))
[docs] def dot(self, other):
""" The dot product of this vector and the `other`. """
assert isinstance(other, Vector3)
return self.x * other.x + \
self.y * other.y + \
self.z * other.z
[docs] def cross(self, other):
""" The cross product of this vector and the `other`. """
assert isinstance(other, Vector3)
return Vector3(self.y * other.z - self.z * other.y,
-self.x * other.z + self.z * other.x,
self.x * other.y - self.y * other.x)
[docs] def reflect(self, normal):
"""
Reflect this vector across a plane with the given `normal`
.. note::
Assumes the given `normal` has unit (1) length.
"""
assert isinstance(normal, Vector3)
assert normal.magnitude_squared() == 1
d = 2 * (self.x * normal.x + self.y * normal.y + self.z * normal.z)
return self.__class__(self.x - d * normal.x,
self.y - d * normal.y,
self.z - d * normal.z)
[docs] def rotate(self, axis, theta):
"""
Return a new vector rotated around `axis` by angle `theta`. Right hand
rule applies.
"""
# Adapted from equations published by Glenn Murray.
# http://inside.mines.edu/~gmurray/ArbitraryAxisRotation/ArbitraryAxisRotation.html
x, y, z = self.x, self.y,self.z
u, v, w = axis.x, axis.y, axis.z
# Extracted common factors for simplicity and efficiency
r2 = u**2 + v**2 + w**2
r = math.sqrt(r2)
ct = math.cos(theta)
st = math.sin(theta) / r
dt = (u*x + v*y + w*z) * (1 - ct) / r2
return Vector3((u * dt + x * ct + (-w * y + v * z) * st),
(v * dt + y * ct + ( w * x - u * z) * st),
(w * dt + z * ct + (-v * x + u * y) * st))
[docs] def angle(self, other):
""" Return the angle to the vector other. """
ratio = self.dot(other) / (self.magnitude()*other.magnitude())
ratio = max(-1.0, min(1.0, ratio))
return math.acos(ratio)
[docs] def project(self, other):
""" Return one vector projected on the vector other. """
n = other.normalized()
return self.dot(n)*n
[docs] def snap(self, grid):
"""
Snaps this vector to a `grid`:
>>> Vector(1.15, 1.15, 0.9).snap(0.25)
Vector(1.25, 1.25, 1.0)
"""
def snap(v):
return round(v / grid) * grid
return self.__class__(snap(self.x), snap(self.y), snap(self.z))
[docs] def point(self):
""" Convert this vector into a point. """
return Point3(self.x, self.y, self.z)
class Basis:
@property
def x(self): return Vector3(1, 0, 0)
@property
def y(self): return Vector3(0, 1, 0)
@property
def z(self): return Vector3(0, 0, 1)
basis = Basis()
generic.Vector.basis = Vector3.basis
Vector = Vector3
Vector3.bx = Vector3.basis.x
Vector3.by = Vector3.basis.y
Vector3.bz = Vector3.basis.z
[docs]class Point3(Vector3, generic.Point, Geometry):
"""
A close cousin of :py:class:`~petrify.space.Vector`, used to represent a
point instead of a transform:
>>> Point(1, 2, 3) + Vector(1, 1, 1)
Point(2, 3, 4)
Defines a convenience `.origin` attribute for this commonly-used point:
>>> Point.origin
Point(0, 0, 0)
"""
def __repr__(self):
return 'Point({0!r}, {1!r}, {2!r})'.format(*self.xyz)
[docs] def intersect(self, other):
"""
Returns whether the point lies within the given `other` sphere:
"""
return other._intersect_point3(self)
def _intersect_sphere(self, other):
return _intersect_point3_sphere(self, other)
[docs] def connect(self, other):
"""
Find the shortest line segment connecting this object to the `other`
object.
"""
return other._connect_point3(self)
def _connect_point3(self, other):
if self != other:
return LineSegment3(other, self)
return None
def _connect_line3(self, other):
c = _connect_point3_line3(self, other)
if c:
return c._swap()
def _connect_sphere(self, other):
c = _connect_point3_sphere(self, other)
if c:
return c._swap()
def _connect_plane(self, other):
c = _connect_point3_plane(self, other)
if c:
return c._swap()
[docs] def vector(self):
""" The vector formed from the origin to this point. """
return Vector3(self.x, self.y, self.z)
Point = Point3
Point3.origin = Point3(0, 0, 0)
generic.Point.origin = Point3.origin