# tuple3d.py # Walker M. White (wmw2) # August 20, 2013 """Module that provides tuple objects for 3D geometry. This module provides classes for tuples, points, vectors. The tuples are the base class, while points and vectors are subclasses.""" import numpy import math import copy class Tuple3D(object): """An instance is a tuple in 3D space. This serves as the base class for both Point and Vector.""" @property def x(self): """The x coordinate **Invariant**: Value must be a float. If assigned an int, it will be typecast to a float (possibly raising a TypeError).""" return self._x @x.setter def x(self, value): self._x = float(value) @x.deleter def x(self): del self._x @property def y(self): """The y coordinate **Invariant**: Value must be a float. If assigned an int, it will be typecast to a float (possibly raising a TypeError).""" return self._y @y.setter def y(self, value): self._y = float(value) @y.deleter def y(self): del self._y @property def z(self): """The z coordinate **Invariant**: Value must be a float. If assigned an int, it will be typecast to a float (possibly raising a TypeError).""" return self._z @z.setter def z(self, value): self._z = float(value) @z.deleter def z(self): del self._z # METHODS def __init__(self, x=0, y=0, z=0): """**Constructor**: creates a new Tuple3D value (x,y,z). :param x: initial x value **Precondition**: value is an int or float. :param y: initial y value **Precondition**: value is an int or float. :param z: initial z value **Precondition**: value is an int or float. All values are 0.0 by default. """ self.x = x self.y = y self.z = z def __eq__(self, other): """**Returns**: True if self and other are equivalent Tuple3Ds. This method uses numpy to test whether the coordinates are "close enough". It does not require exact equality for floats. :param other: value to compare against """ return (type(other) == Tuple3D and numpy.allclose(self.list(),other.list())) def __ne__(self, other): """**Returns**: True if self and other are not equivalent Tuple3Ds. :param other: value to compare against """ return not self == other def __str__(self): """**Returns**: Readable String representation of this Tuple3D. """ return "("+str(self.x)+","+str(self.y)+","+str(self.z)+")" def __repr__(self): """**Returns**: Unambiguous String representation of this Tuple3D. """ return "%s%s" % (self.__class__,self.__str__()) def list(self): """**Returns**: A python list with the contents of this Tuple3D.""" return [self.x,self.y,self.z] def abs(self): """Sets each component of this Tuple3D to its absolute value.""" self.x = abs(self.x) self.y = abs(self.y) self.z = abs(self.z) def clamp(self,low,high): """Clamps this tuple to the range [low, high]. Any value in this Vector less than low is set to low. Any value greater than high is set to high.""" self.x = max(low,min(high,self.x)) self.y = max(low,min(high,self.y)) self.z = max(low,min(high,self.z)) def __add__(self, other): """**Returns**: the sum of self and other. The value returned has the same type as self (so it is either a Tuple3D or is a subclass of Tuple3D). The contents of this object are not altered. :param other: tuple value to add **Precondition**: value has the same type as self. """ assert (type(other) == type(self)), "value %(value)s is not a of type %(type)s" % {'value': `other`, 'type':`type(self)`} result = copy.copy(self) result.x += other.x result.y += other.y result.z += other.z return result def __rmul__(self, scalar): """**Returns**: the scalar multiple of self and other. The value returned is a new Tuple3D. The contents of this Tuple3D are not altered. :param scalar: scalar to multiply by **Precondition**: value is an int or float. """ assert (type(scalar) in [int,float]), "value %s is not a number" % `scalar` result = copy.copy(self) result.x *= scalar result.y *= scalar result.z *= scalar return result def interpolate(self, other, alpha): """**Returns**: the interpolation of self and other via alpha. The value returned has the same type as self (so it is either a Tuple3D or is a subclass of Tuple3D). The contents of this object are not altered. The resulting value is alpha*self+(1-alpha)*other according to Tuple3D addition and scalar multiplication. :param other: tuple value to interpolate with **Precondition**: value has the same type as self. :param alpha: scalar to interpolate by **Precondition**: value is an int or float. """ assert (type(other) == type(self)), "value %(value)s is not a of type %(type)s" % {'value': `other`, 'type':`type(self)`} assert (type(alpha) in [int,float]), "value %s is not a number" % `alpha` return alpha*self+(1-alpha)*other class Point(Tuple3D): """An instance is a point in 3D space""" # METHODS def __init__(self, x=0, y=0, z=0): """**Constructor**: creates a new Point value (x,y,z). :param x: initial x value **Precondition**: value is an int or float. :param y: initial y value **Precondition**: value is an int or float. :param z: initial z value **Precondition**: value is an int or float. All values are 0.0 by default. """ super(Point,self).__init__(x,y,z) def __eq__(self, other): """**Returns**: True if self and other are equivalent Points. This method uses numpy to test whether the coordinates are "close enough". It does not require exact equality for floats. :param other: value to compare against """ return (type(other) == Point and numpy.allclose(self.list(),other.list())) def distanceTo(self, other): """**Returns**: the Euclidean distance from this point to other :param other: value to compare against **Precondition**: value is a Tuple3D object. """ return math.sqrt((self.x-other.x)*(self.x-other.x)+ (self.y-other.y)*(self.y-other.y)+ (self.z-other.z)*(self.z-other.z)) def __sub__(self, tail): """**Returns**: the Vector from tail to self. The value returned is a Vector with this point at its head. :param tail: the tail value for the new Vector **Precondition**: value is a Point object. """ assert (type(tail) == Point), "value %s is not a Point" % `tail` return Vector(self.x-tail.x,self.y-tail.y,self.z-tail.z) class Vector(Tuple3D): """An instance is a Vector in 3D space""" # METHODS def __init__(self, x=0, y=0, z=0): """**Constructor**: creates a new Vector object (x,y,z). :param x: initial x value **Precondition**: value is an int or float. :param y: initial y value **Precondition**: value is an int or float. :param z: initial z value **Precondition**: value is an int or float. All values are 0.0 by default. """ super(Vector,self).__init__(x,y,z) def __eq__(self, other): """**Returns**: True if self and other are equivalent Vectors. This method uses numpy to test whether the coordinates are "close enough". It does not require exact equality for floats. :param other: value to compare against """ return (type(other) == Vector and numpy.allclose(self.list(),other.list())) def __str__(self): """**Returns**: Readable String representation of this Vector. """ return "<"+str(self.x)+","+str(self.y)+","+str(self.z)+">" def length(self): """**Returns**: the length of this Vector.""" return math.sqrt(self.x*self.x+self.y*self.y+self.z*self.z) def __sub__(self, other): """**Returns**: the difference between this Vector and other. The value returned is a new Vector. The contents of this vector are not modified. :param other: the Vector to subtract **Precondition**: value is a Vector object. """ assert (type(other) == Vector), "value %s is not a Vector" % `other` return Vector(self.x-other.x,self.y-other.y,self.z-other.z) def angle(self,other): """**Returns**: the angle between this vector and other. The answer provided is in radians. Neither this Vector nor other may be the zero vector. :param other: value to compare against **Precondition**: value a nonzero Vector object. """ assert (type(other) == Vector), "value %s is not a Vector" % `other` na = self.length() nb = other.length() assert (na != 0), "Vector %s is zero" % `self` assert (nb != 0), "Vector %s is zero" % `other` return math.acos(self.dot(other)/(na*nb)) def cross(self,other): """**Returns**: the cross product between self and other. The result of this method is a new Vector :param other: value to cross **Precondition**: value a Vector object. """ assert (type(other) == Vector), "value %s is not a Vector" % `other` return Vector(self.y*other.z-self.z*other.y, self.z*other.x-self.z*other.z, self.x*other.y-self.y*other.x) def dot(self,other): """**Returns**: the dot product between self and other. The result of this method is a float. :param other: value to dot **Precondition**: value a Vector object. """ assert (type(other) == Vector), "value %s is not a Vector" % `other` return (self.x*other.x+self.y*other.y+self.z*other.z) def normalize(self): """Normalizes this Vector in place. This method alters the Vector so that it has the same direction, but its length is now 1. """ length = self.length() self.x /= length self.y /= length self.z /= length