view bGrease/geometry.py @ 115:26e65f27dbd7

"Open" will now not be shown in the context menu when the lockable is locked. "Lock" will not not be shown in the context menu when the lockable is open.
author KarstenBock@gmx.net
date Mon, 03 Oct 2011 14:12:17 +0200
parents ff3e395abf91
children a6bbb732b27b
line wrap: on
line source

__version__ = "$Id$"
__docformat__ = "reStructuredText"

import operator
import math
import ctypes 

class Vec2d(ctypes.Structure):
    """2d vector class, supports vector and scalar operators,
       and also provides a bunch of high level functions
       """
    __slots__ = ['x', 'y']
    
    @classmethod
    def from_param(cls, arg):
        return cls(arg)
        
    def __init__(self, x_or_pair, y = None):
        
        if y == None:
            self.x = x_or_pair[0]
            self.y = x_or_pair[1]
        else:
            self.x = x_or_pair
            self.y = y
 
    def __len__(self):
        return 2
 
    def __getitem__(self, key):
        if key == 0:
            return self.x
        elif key == 1:
            return self.y
        else:
            raise IndexError("Invalid subscript "+str(key)+" to Vec2d")
 
    def __setitem__(self, key, value):
        if key == 0:
            self.x = value
        elif key == 1:
            self.y = value
        else:
            raise IndexError("Invalid subscript "+str(key)+" to Vec2d")
 
    # String representaion (for debugging)
    def __repr__(self):
        return 'Vec2d(%s, %s)' % (self.x, self.y)
    
    # Comparison
    def __eq__(self, other):
        if hasattr(other, "__getitem__") and len(other) == 2:
            return self.x == other[0] and self.y == other[1]
        else:
            return False
    
    def __ne__(self, other):
        if hasattr(other, "__getitem__") and len(other) == 2:
            return self.x != other[0] or self.y != other[1]
        else:
            return True
 
    def __nonzero__(self):
        return self.x or self.y
 
    # Generic operator handlers
    def _o2(self, other, f):
        "Any two-operator operation where the left operand is a Vec2d"
        if isinstance(other, Vec2d):
            return Vec2d(f(self.x, other.x),
                         f(self.y, other.y))
        elif (hasattr(other, "__getitem__")):
            return Vec2d(f(self.x, other[0]),
                         f(self.y, other[1]))
        else:
            return Vec2d(f(self.x, other),
                         f(self.y, other))
 
    def _r_o2(self, other, f):
        "Any two-operator operation where the right operand is a Vec2d"
        if (hasattr(other, "__getitem__")):
            return Vec2d(f(other[0], self.x),
                         f(other[1], self.y))
        else:
            return Vec2d(f(other, self.x),
                         f(other, self.y))
 
    def _io(self, other, f):
        "inplace operator"
        if (hasattr(other, "__getitem__")):
            self.x = f(self.x, other[0])
            self.y = f(self.y, other[1])
        else:
            self.x = f(self.x, other)
            self.y = f(self.y, other)
        return self
 
    # Addition
    def __add__(self, other):
        if isinstance(other, Vec2d):
            return Vec2d(self.x + other.x, self.y + other.y)
        elif hasattr(other, "__getitem__"):
            return Vec2d(self.x + other[0], self.y + other[1])
        else:
            return Vec2d(self.x + other, self.y + other)
    __radd__ = __add__
    
    def __iadd__(self, other):
        if isinstance(other, Vec2d):
            self.x += other.x
            self.y += other.y
        elif hasattr(other, "__getitem__"):
            self.x += other[0]
            self.y += other[1]
        else:
            self.x += other
            self.y += other
        return self
 
    # Subtraction
    def __sub__(self, other):
        if isinstance(other, Vec2d):
            return Vec2d(self.x - other.x, self.y - other.y)
        elif (hasattr(other, "__getitem__")):
            return Vec2d(self.x - other[0], self.y - other[1])
        else:
            return Vec2d(self.x - other, self.y - other)
    def __rsub__(self, other):
        if isinstance(other, Vec2d):
            return Vec2d(other.x - self.x, other.y - self.y)
        if (hasattr(other, "__getitem__")):
            return Vec2d(other[0] - self.x, other[1] - self.y)
        else:
            return Vec2d(other - self.x, other - self.y)
    def __isub__(self, other):
        if isinstance(other, Vec2d):
            self.x -= other.x
            self.y -= other.y
        elif (hasattr(other, "__getitem__")):
            self.x -= other[0]
            self.y -= other[1]
        else:
            self.x -= other
            self.y -= other
        return self
 
    # Multiplication
    def __mul__(self, other):
        if isinstance(other, Vec2d):
            return Vec2d(self.x*other.y, self.y*other.y)
        if (hasattr(other, "__getitem__")):
            return Vec2d(self.x*other[0], self.y*other[1])
        else:
            return Vec2d(self.x*other, self.y*other)
    __rmul__ = __mul__
    
    def __imul__(self, other):
        if isinstance(other, Vec2d):
            self.x *= other.x
            self.y *= other.y
        elif (hasattr(other, "__getitem__")):
            self.x *= other[0]
            self.y *= other[1]
        else:
            self.x *= other
            self.y *= other
        return self
 
    # Division
    def __div__(self, other):
        return self._o2(other, operator.div)
    def __rdiv__(self, other):
        return self._r_o2(other, operator.div)
    def __idiv__(self, other):
        return self._io(other, operator.div)
 
    def __floordiv__(self, other):
        return self._o2(other, operator.floordiv)
    def __rfloordiv__(self, other):
        return self._r_o2(other, operator.floordiv)
    def __ifloordiv__(self, other):
        return self._io(other, operator.floordiv)
 
    def __truediv__(self, other):
        return self._o2(other, operator.truediv)
    def __rtruediv__(self, other):
        return self._r_o2(other, operator.truediv)
    def __itruediv__(self, other):
        return self._io(other, operator.floordiv)
 
    # Modulo
    def __mod__(self, other):
        return self._o2(other, operator.mod)
    def __rmod__(self, other):
        return self._r_o2(other, operator.mod)
 
    def __divmod__(self, other):
        return self._o2(other, divmod)
    def __rdivmod__(self, other):
        return self._r_o2(other, divmod)
 
    # Exponentation
    def __pow__(self, other):
        return self._o2(other, operator.pow)
    def __rpow__(self, other):
        return self._r_o2(other, operator.pow)
 
    # Bitwise operators
    def __lshift__(self, other):
        return self._o2(other, operator.lshift)
    def __rlshift__(self, other):
        return self._r_o2(other, operator.lshift)
 
    def __rshift__(self, other):
        return self._o2(other, operator.rshift)
    def __rrshift__(self, other):
        return self._r_o2(other, operator.rshift)
 
    def __and__(self, other):
        return self._o2(other, operator.and_)
    __rand__ = __and__
 
    def __or__(self, other):
        return self._o2(other, operator.or_)
    __ror__ = __or__
 
    def __xor__(self, other):
        return self._o2(other, operator.xor)
    __rxor__ = __xor__
 
    # Unary operations
    def __neg__(self):
        return Vec2d(operator.neg(self.x), operator.neg(self.y))
 
    def __pos__(self):
        return Vec2d(operator.pos(self.x), operator.pos(self.y))
 
    def __abs__(self):
        return Vec2d(abs(self.x), abs(self.y))
 
    def __invert__(self):
        return Vec2d(-self.x, -self.y)
 
    # vectory functions
    def get_length_sqrd(self): 
        """Get the squared length of the vector.
        It is more efficent to use this method instead of first call 
        get_length() or access .length and then do a sqrt().
        
        :return: The squared length
        """
        return self.x**2 + self.y**2
 
    def get_length(self):
        """Get the length of the vector.
        
        :return: The length
        """
        return math.sqrt(self.x**2 + self.y**2)    
    def __setlength(self, value):
        length = self.get_length()
        self.x *= value/length
        self.y *= value/length
    length = property(get_length, __setlength, doc = """Gets or sets the magnitude of the vector""")
       
    def rotate(self, angle_degrees):
        """Rotate the vector by angle_degrees degrees clockwise."""
        radians = -math.radians(angle_degrees)
        cos = math.cos(radians)
        sin = math.sin(radians)
        x = self.x*cos - self.y*sin
        y = self.x*sin + self.y*cos
        self.x = x
        self.y = y
 
    def rotated(self, angle_degrees):
        """Create and return a new vector by rotating this vector by 
        angle_degrees degrees clockwise.
        
        :return: Rotated vector
        """
        radians = -math.radians(angle_degrees)
        cos = math.cos(radians)
        sin = math.sin(radians)
        x = self.x*cos - self.y*sin
        y = self.x*sin + self.y*cos
        return Vec2d(x, y)
    
    def get_angle(self):
        if (self.get_length_sqrd() == 0):
            return 0
        return math.degrees(math.atan2(self.y, self.x))
    def __setangle(self, angle_degrees):
        self.x = self.length
        self.y = 0
        self.rotate(angle_degrees)
    angle = property(get_angle, __setangle, doc="""Gets or sets the angle of a vector""")
 
    def get_angle_between(self, other):
        """Get the angle between the vector and the other in degrees
        
        :return: The angle
        """
        cross = self.x*other[1] - self.y*other[0]
        dot = self.x*other[0] + self.y*other[1]
        return math.degrees(math.atan2(cross, dot))
            
    def normalized(self):
        """Get a normalized copy of the vector
        
        :return: A normalized vector
        """
        length = self.length
        if length != 0:
            return self/length
        return Vec2d(self)
 
    def normalize_return_length(self):
        """Normalize the vector and return its length before the normalization
        
        :return: The length before the normalization
        """
        length = self.length
        if length != 0:
            self.x /= length
            self.y /= length
        return length
 
    def perpendicular(self):
        return Vec2d(-self.y, self.x)
    
    def perpendicular_normal(self):
        length = self.length
        if length != 0:
            return Vec2d(-self.y/length, self.x/length)
        return Vec2d(self)
        
    def dot(self, other):
        """The dot product between the vector and other vector
            v1.dot(v2) -> v1.x*v2.x + v1.y*v2.y
            
        :return: The dot product
        """
        return float(self.x*other[0] + self.y*other[1])
        
    def get_distance(self, other):
        """The distance between the vector and other vector
        
        :return: The distance
        """
        return math.sqrt((self.x - other[0])**2 + (self.y - other[1])**2)
        
    def get_dist_sqrd(self, other):
        """The squared distance between the vector and other vector
        It is more efficent to use this method than to call get_distance()
        first and then do a sqrt() on the result.
        
        :return: The squared distance
        """
        return (self.x - other[0])**2 + (self.y - other[1])**2
        
    def projection(self, other):
        other_length_sqrd = other[0]*other[0] + other[1]*other[1]
        projected_length_times_other_length = self.dot(other)
        return other*(projected_length_times_other_length/other_length_sqrd)
    
    def cross(self, other):
        """The cross product between the vector and other vector
            v1.cross(v2) -> v1.x*v2.y - v2.y-v1.x
        
        :return: The cross product
        """
        return self.x*other[1] - self.y*other[0]
    
    def interpolate_to(self, other, range):
        return Vec2d(self.x + (other[0] - self.x)*range, self.y + (other[1] - self.y)*range)
    
    def convert_to_basis(self, x_vector, y_vector):
        return Vec2d(self.dot(x_vector)/x_vector.get_length_sqrd(), self.dot(y_vector)/y_vector.get_length_sqrd())
 
    # Extra functions, mainly for chipmunk
    def cpvrotate(self, other):
        return Vec2d(self.x*other.x - self.y*other.y, self.x*other.y + self.y*other.x)
    def cpvunrotate(self, other):
        return Vec2d(self.x*other.x + self.y*other.y, self.y*other.x - self.x*other.y)
    
    # Pickle, does not work atm.
    def __getstate__(self):
        return [self.x, self.y]
        
    def __setstate__(self, dict):
        self.x, self.y = dict
    def __newobj__(cls, *args):
        return cls.__new__(cls, *args)    
Vec2d._fields_ = [
            ('x', ctypes.c_double),
            ('y', ctypes.c_double),
        ]


class Vec2dArray(list):

	def __init__(self, iterable=()):
		list.__init__(self, (Vec2d(i) for i in iterable))

	def __setitem__(self, index, value):
		list.__setitem__(self, index, Vec2d(value))
	
	def append(self, value):
		"""Append a vector to the array"""
		list.append(self, Vec2d(value))
	
	def insert(self, index, value):
		"""Insert a vector into the array"""
		list.insert(self, index, Vec2d(value))
	
	def transform(self, offset=Vec2d(0,0), angle=0, scale=1.0):
		"""Return a new transformed Vec2dArray"""
		offset = Vec2d(offset)
		angle = math.radians(-angle)
		rot_vec = Vec2d(math.cos(angle), math.sin(angle))
		xformed = Vec2dArray()
		for vec in self:
			xformed.append(vec.cpvrotate(rot_vec) * scale + offset)
		return xformed
	
	def segments(self, closed=True):
		"""Generate arrays of line segments connecting adjacent vetices
		in this array, exploding the shape into it's constituent segments
		"""
		if len(self) >= 2:
			last = self[0]
			for vert in self[1:]:
				yield Vec2dArray((last, vert))
				last = vert
			if closed:
				yield Vec2dArray((last, self[0]))
		elif self and closed:
			yield Vec2dArray((self[0], self[0]))



class Rect(ctypes.Structure):
	"""Simple rectangle. Will gain more functionality as needed"""
	_fields_ = [
		('left', ctypes.c_double),
		('top', ctypes.c_double),
		('right', ctypes.c_double),
		('bottom', ctypes.c_double),
	]

	def __init__(self, rect_or_left, bottom=None, right=None, top=None):
		if bottom is not None:
			assert right is not None and top is not None, "No enough arguments to Rect"
			self.left = rect_or_left
			self.bottom = bottom
			self.right = right
			self.top = top
		else:
			self.left = rect_or_left.left
			self.bottom = rect_or_left.bottom
			self.right = rect_or_left.right
			self.top = rect_or_left.top

	@property
	def width(self):
		"""Rectangle width"""
		return self.right - self.left
	
	@property
	def height(self):
		"""Rectangle height"""
		return self.top - self.bottom


########################################################################
## Unit Testing                                                       ##
########################################################################
if __name__ == "__main__":
 
    import unittest
    import pickle
 
    ####################################################################
    class UnitTestVec2d(unittest.TestCase):
    
        def setUp(self):
            pass
        
        def testCreationAndAccess(self):
            v = Vec2d(111, 222)
            self.assert_(v.x == 111 and v.y == 222)
            v.x = 333
            v[1] = 444
            self.assert_(v[0] == 333 and v[1] == 444)
 
        def testMath(self):
            v = Vec2d(111,222)
            self.assertEqual(v + 1, Vec2d(112, 223))
            self.assert_(v - 2 == [109, 220])
            self.assert_(v * 3 == (333, 666))
            self.assert_(v / 2.0 == Vec2d(55.5, 111))
            #self.assert_(v / 2 == (55, 111)) # Not supported since this is a c_float structure in the bottom
            self.assert_(v ** Vec2d(2, 3) == [12321, 10941048])
            self.assert_(v + [-11, 78] == Vec2d(100, 300))
            #self.assert_(v / [11,2] == [10,111]) # Not supported since this is a c_float structure in the bottom
 
        def testReverseMath(self):
            v = Vec2d(111, 222)
            self.assert_(1 + v == Vec2d(112, 223))
            self.assert_(2 - v == [-109, -220])
            self.assert_(3 * v == (333, 666))
            #self.assert_([222,999] / v == [2,4]) # Not supported since this is a c_float structure in the bottom
            self.assert_([111, 222] ** Vec2d(2, 3) == [12321, 10941048])
            self.assert_([-11, 78] + v == Vec2d(100, 300))
 
        def testUnary(self):
            v = Vec2d(111, 222)
            v = -v
            self.assert_(v == [-111, -222])
            v = abs(v)
            self.assert_(v == [111, 222])
 
        def testLength(self):
            v = Vec2d(3,4)
            self.assert_(v.length == 5)
            self.assert_(v.get_length_sqrd() == 25)
            self.assert_(v.normalize_return_length() == 5)
            self.assertAlmostEquals(v.length, 1)
            v.length = 5
            self.assert_(v == Vec2d(3, 4))
            v2 = Vec2d(10, -2)
            self.assert_(v.get_distance(v2) == (v - v2).get_length())
            
        def testAngles(self):            
            v = Vec2d(0, 3)
            self.assertEquals(v.angle, 90)
            v2 = Vec2d(v)
            v.rotate(-90)
            self.assertEqual(v.get_angle_between(v2), 90)
            v2.angle -= 90
            self.assertEqual(v.length, v2.length)
            self.assertEquals(v2.angle, 0)
            self.assertEqual(v2, [3, 0])
            self.assert_((v - v2).length < .00001)
            self.assertEqual(v.length, v2.length)
            v2.rotate(300)
            self.assertAlmostEquals(v.get_angle_between(v2), -60, 5) # Allow a little more error than usual (floats..)
            v2.rotate(v2.get_angle_between(v))
            angle = v.get_angle_between(v2)
            self.assertAlmostEquals(v.get_angle_between(v2), 0)  
 
        def testHighLevel(self):
            basis0 = Vec2d(5.0, 0)
            basis1 = Vec2d(0, .5)
            v = Vec2d(10, 1)
            self.assert_(v.convert_to_basis(basis0, basis1) == [2, 2])
            self.assert_(v.projection(basis0) == (10, 0))
            self.assert_(basis0.dot(basis1) == 0)
            
        def testCross(self):
            lhs = Vec2d(1, .5)
            rhs = Vec2d(4, 6)
            self.assert_(lhs.cross(rhs) == 4)
            
        def testComparison(self):
            int_vec = Vec2d(3, -2)
            flt_vec = Vec2d(3.0, -2.0)
            zero_vec = Vec2d(0, 0)
            self.assert_(int_vec == flt_vec)
            self.assert_(int_vec != zero_vec)
            self.assert_((flt_vec == zero_vec) == False)
            self.assert_((flt_vec != int_vec) == False)
            self.assert_(int_vec == (3, -2))
            self.assert_(int_vec != [0, 0])
            self.assert_(int_vec != 5)
            self.assert_(int_vec != [3, -2, -5])
        
        def testInplace(self):
            inplace_vec = Vec2d(5, 13)
            inplace_ref = inplace_vec
            inplace_src = Vec2d(inplace_vec)    
            inplace_vec *= .5
            inplace_vec += .5
            inplace_vec /= (3, 6)
            inplace_vec += Vec2d(-1, -1)
            alternate = (inplace_src*.5 + .5)/Vec2d(3, 6) + [-1, -1]
            self.assertEquals(inplace_vec, inplace_ref)
            self.assertEquals(inplace_vec, alternate)
        
        def testPickle(self):
            return # pickling does not work atm
            testvec = Vec2d(5, .3)
            testvec_str = pickle.dumps(testvec)
            loaded_vec = pickle.loads(testvec_str)
            self.assertEquals(testvec, loaded_vec)
    
    ####################################################################
    unittest.main()