1335 lines
46 KiB
Python
1335 lines
46 KiB
Python
|
import doctest
|
||
|
import collections
|
||
|
|
||
|
# TODO - finish doc tests
|
||
|
|
||
|
# TODO - unit tests needed for get/set and Box named tuple
|
||
|
|
||
|
__version__ = '0.1.4'
|
||
|
|
||
|
|
||
|
# Constants for rectangle attributes:
|
||
|
TOP = 'top'
|
||
|
BOTTOM = 'bottom'
|
||
|
LEFT = 'left'
|
||
|
RIGHT = 'right'
|
||
|
TOPLEFT = 'topleft'
|
||
|
TOPRIGHT = 'topright'
|
||
|
BOTTOMLEFT = 'bottomleft'
|
||
|
BOTTOMRIGHT = 'bottomright'
|
||
|
MIDTOP = 'midtop'
|
||
|
MIDRIGHT = 'midright'
|
||
|
MIDLEFT = 'midleft'
|
||
|
MIDBOTTOM = 'midbottom'
|
||
|
CENTER = 'center'
|
||
|
CENTERX = 'centerx'
|
||
|
CENTERY = 'centery'
|
||
|
WIDTH = 'width'
|
||
|
HEIGHT = 'height'
|
||
|
SIZE = 'size'
|
||
|
BOX = 'box'
|
||
|
AREA = 'area'
|
||
|
|
||
|
Box = collections.namedtuple('Box', 'left top width height')
|
||
|
Point = collections.namedtuple('Point', 'x y')
|
||
|
Size = collections.namedtuple('Size', 'width height')
|
||
|
|
||
|
class PyRectException(Exception):
|
||
|
"""
|
||
|
This class exists for PyRect exceptions. If the PyRect module raises any
|
||
|
non-PyRectException exceptions, this indicates there's a bug in PyRect.
|
||
|
"""
|
||
|
pass
|
||
|
|
||
|
|
||
|
def _checkForIntOrFloat(arg):
|
||
|
"""Raises an exception if arg is not an int or float. Always returns None."""
|
||
|
if not isinstance(arg, (int, float)):
|
||
|
raise PyRectException('argument must be int or float, not %s' % (arg.__class__.__name__))
|
||
|
|
||
|
|
||
|
def _checkForInt(arg):
|
||
|
"""Raises an exception if arg is not an int. Always returns None."""
|
||
|
if not isinstance(arg, int):
|
||
|
raise PyRectException('argument must be int or float, not %s' % (arg.__class__.__name__))
|
||
|
|
||
|
|
||
|
def _checkForTwoIntOrFloatTuple(arg):
|
||
|
try:
|
||
|
if not isinstance(arg[0], (int, float)) or \
|
||
|
not isinstance(arg[1], (int, float)):
|
||
|
raise PyRectException('argument must be a two-item tuple containing int or float values')
|
||
|
except:
|
||
|
raise PyRectException('argument must be a two-item tuple containing int or float values')
|
||
|
|
||
|
|
||
|
def _checkForFourIntOrFloatTuple(arg):
|
||
|
try:
|
||
|
if not isinstance(arg[0], (int, float)) or \
|
||
|
not isinstance(arg[1], (int, float)) or \
|
||
|
not isinstance(arg[2], (int, float)) or \
|
||
|
not isinstance(arg[3], (int, float)):
|
||
|
raise PyRectException('argument must be a four-item tuple containing int or float values')
|
||
|
except:
|
||
|
raise PyRectException('argument must be a four-item tuple containing int or float values')
|
||
|
|
||
|
|
||
|
def _collides(rectOrPoint1, rectOrPoint2):
|
||
|
"""Returns True if rectOrPoint1 and rectOrPoint2 collide with each other."""
|
||
|
|
||
|
|
||
|
def _getRectsAndPoints(rectsOrPoints):
|
||
|
points = []
|
||
|
rects = []
|
||
|
for rectOrPoint in rectsOrPoints:
|
||
|
try:
|
||
|
_checkForTwoIntOrFloatTuple(rectOrPoint)
|
||
|
points.append(rectOrPoint)
|
||
|
except PyRectException:
|
||
|
try:
|
||
|
_checkForFourIntOrFloatTuple(rectOrPoint)
|
||
|
except:
|
||
|
raise PyRectException('argument is not a point or a rect tuple')
|
||
|
rects.append(rectOrPoint)
|
||
|
return (rects, points)
|
||
|
|
||
|
|
||
|
'''
|
||
|
def collideAnyBetween(rectsOrPoints):
|
||
|
"""Returns True if any of the (x, y) or (left, top, width, height) tuples
|
||
|
in rectsOrPoints collides with any other point or box tuple in rectsOrPoints.
|
||
|
|
||
|
>>> p1 = (50, 50)
|
||
|
>>> p2 = (100, 100)
|
||
|
>>> p3 = (50, 200)
|
||
|
>>> r1 = (-50, -50, 20, 20)
|
||
|
>>> r2 = (25, 25, 50, 50)
|
||
|
>>> collideAnyBetween([p1, p2, p3, r1, r2]) # p1 and r2 collide
|
||
|
True
|
||
|
>>> collideAnyBetween([p1, p2, p3, r1])
|
||
|
False
|
||
|
"""
|
||
|
# TODO - needs to be complete
|
||
|
|
||
|
# split up
|
||
|
rects, points = _getRectsAndPoints(rectsOrPoints)
|
||
|
|
||
|
# compare points with each other
|
||
|
if len(points) > 1:
|
||
|
for point in points:
|
||
|
if point != points[0]:
|
||
|
return False
|
||
|
|
||
|
# TODO finish
|
||
|
'''
|
||
|
|
||
|
|
||
|
|
||
|
'''
|
||
|
def collideAllBetween(rectsOrPoints):
|
||
|
"""Returns True if any of the (x, y) or (left, top, width, height) tuples
|
||
|
in rectsOrPoints collides with any other point or box tuple in rectsOrPoints.
|
||
|
|
||
|
>>> p1 = (50, 50)
|
||
|
>>> p2 = (100, 100)
|
||
|
>>> p3 = (50, 200)
|
||
|
>>> r1 = (-50, -50, 20, 20)
|
||
|
>>> r2 = (25, 25, 50, 50)
|
||
|
>>> collideAllBetween([p1, p2, p3, r1, r2])
|
||
|
False
|
||
|
>>> collideAllBetween([p1, p2, p3, r1])
|
||
|
False
|
||
|
>>> collideAllBetween([p1, r2]) # Everything in the list collides with each other.
|
||
|
True
|
||
|
"""
|
||
|
|
||
|
# Check for valid arguments
|
||
|
try:
|
||
|
for rectOrPoint in rectsOrPoints:
|
||
|
if len(rectOrPoint) == 2:
|
||
|
_checkForTwoIntOrFloatTuple(rectOrPoint)
|
||
|
elif len(rectOrPoint) == 4:
|
||
|
_checkForFourIntOrFloatTuple(rectOrPoint)
|
||
|
else:
|
||
|
raise PyRectException()
|
||
|
except:
|
||
|
raise PyRectException('Arguments in rectsOrPoints must be 2- or 4-integer/float tuples.')
|
||
|
|
||
|
raise NotImplementedError # return a list of all rects or points that collide with any other in the argument
|
||
|
'''
|
||
|
|
||
|
class Rect(object):
|
||
|
def __init__(self, left=0, top=0, width=0, height=0, enableFloat=False, readOnly=False, onChange=None, onRead=None):
|
||
|
_checkForIntOrFloat(width)
|
||
|
_checkForIntOrFloat(height)
|
||
|
_checkForIntOrFloat(left)
|
||
|
_checkForIntOrFloat(top)
|
||
|
|
||
|
self._enableFloat = bool(enableFloat)
|
||
|
self._readOnly = bool(readOnly)
|
||
|
|
||
|
if onChange is not None and not callable(onChange):
|
||
|
raise PyRectException('onChange argument must be None or callable (function, method, etc.)')
|
||
|
self.onChange = onChange
|
||
|
|
||
|
if onRead is not None and not callable(onRead):
|
||
|
raise PyRectException('onRead argument must be None or callable (function, method, etc.)')
|
||
|
self.onRead = onRead
|
||
|
|
||
|
if enableFloat:
|
||
|
self._width = float(width)
|
||
|
self._height = float(height)
|
||
|
self._left = float(left)
|
||
|
self._top = float(top)
|
||
|
else:
|
||
|
self._width = int(width)
|
||
|
self._height = int(height)
|
||
|
self._left = int(left)
|
||
|
self._top = int(top)
|
||
|
|
||
|
|
||
|
# OPERATOR OVERLOADING / DUNDER METHODS
|
||
|
def __repr__(self):
|
||
|
"""Return a string of the constructor function call to create this Rect object."""
|
||
|
return '%s(left=%s, top=%s, width=%s, height=%s)' % (self.__class__.__name__, self._left, self._top, self._width, self._height)
|
||
|
|
||
|
|
||
|
def __str__(self):
|
||
|
"""Return a string representation of this Rect object."""
|
||
|
return '(x=%s, y=%s, w=%s, h=%s)' % (self._left, self._top, self._width, self._height)
|
||
|
|
||
|
|
||
|
def callOnChange(self, oldLeft, oldTop, oldWidth, oldHeight):
|
||
|
# Note: callOnChange() should be called *after* the attribute has been changed.
|
||
|
# Note: This isn't thread safe; the attributes can change between the calling of this function and the code in the function running.
|
||
|
if self.onChange is not None:
|
||
|
self.onChange(Box(oldLeft, oldTop, oldWidth, oldHeight), Box(self._left, self._top, self._width, self._height))
|
||
|
|
||
|
|
||
|
@property
|
||
|
def enableFloat(self):
|
||
|
"""
|
||
|
A Boolean attribute that determines if this rectangle uses floating point
|
||
|
numbers for its position and size. False, by default.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.enableFloat
|
||
|
False
|
||
|
>>> r.enableFloat = True
|
||
|
>>> r.top = 3.14
|
||
|
>>> r
|
||
|
Rect(left=0.0, top=3.14, width=10.0, height=20.0)
|
||
|
"""
|
||
|
return self._enableFloat
|
||
|
|
||
|
@enableFloat.setter
|
||
|
def enableFloat(self, value):
|
||
|
if not isinstance(value, bool):
|
||
|
raise PyRectException('enableFloat must be set to a bool value')
|
||
|
self._enableFloat = value
|
||
|
|
||
|
if self._enableFloat:
|
||
|
self._left = float(self._left)
|
||
|
self._top = float(self._top)
|
||
|
self._width = float(self._width)
|
||
|
self._height = float(self._height)
|
||
|
else:
|
||
|
self._left = int(self._left)
|
||
|
self._top = int(self._top)
|
||
|
self._width = int(self._width)
|
||
|
self._height = int(self._height)
|
||
|
|
||
|
|
||
|
# LEFT SIDE PROPERTY
|
||
|
@property
|
||
|
def left(self):
|
||
|
"""
|
||
|
The x coordinate for the left edge of the rectangle. `x` is an alias for `left`.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.left
|
||
|
0
|
||
|
>>> r.left = 50
|
||
|
>>> r
|
||
|
Rect(left=50, top=0, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(LEFT)
|
||
|
return self._left
|
||
|
|
||
|
@left.setter
|
||
|
def left(self, newLeft):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForIntOrFloat(newLeft)
|
||
|
if newLeft != self._left: # Only run this code if the size/position has changed.
|
||
|
originalLeft = self._left
|
||
|
if self._enableFloat:
|
||
|
self._left = newLeft
|
||
|
else:
|
||
|
self._left = int(newLeft)
|
||
|
self.callOnChange(originalLeft, self._top, self._width, self._height)
|
||
|
|
||
|
x = left # x is an alias for left
|
||
|
|
||
|
|
||
|
# TOP SIDE PROPERTY
|
||
|
@property
|
||
|
def top(self):
|
||
|
"""
|
||
|
The y coordinate for the top edge of the rectangle. `y` is an alias for `top`.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.top
|
||
|
0
|
||
|
>>> r.top = 50
|
||
|
>>> r
|
||
|
Rect(left=0, top=50, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(TOP)
|
||
|
return self._top
|
||
|
|
||
|
@top.setter
|
||
|
def top(self, newTop):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForIntOrFloat(newTop)
|
||
|
if newTop != self._top: # Only run this code if the size/position has changed.
|
||
|
originalTop = self._top
|
||
|
if self._enableFloat:
|
||
|
self._top = newTop
|
||
|
else:
|
||
|
self._top = int(newTop)
|
||
|
self.callOnChange(self._left, originalTop, self._width, self._height)
|
||
|
|
||
|
y = top # y is an alias for top
|
||
|
|
||
|
|
||
|
# RIGHT SIDE PROPERTY
|
||
|
@property
|
||
|
def right(self):
|
||
|
"""
|
||
|
The x coordinate for the right edge of the rectangle.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.right
|
||
|
10
|
||
|
>>> r.right = 50
|
||
|
>>> r
|
||
|
Rect(left=40, top=0, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(RIGHT)
|
||
|
return self._left + self._width
|
||
|
|
||
|
@right.setter
|
||
|
def right(self, newRight):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForIntOrFloat(newRight)
|
||
|
if newRight != self._left + self._width: # Only run this code if the size/position has changed.
|
||
|
originalLeft = self._left
|
||
|
if self._enableFloat:
|
||
|
self._left = newRight - self._width
|
||
|
else:
|
||
|
self._left = int(newRight) - self._width
|
||
|
self.callOnChange(originalLeft, self._top, self._width, self._height)
|
||
|
|
||
|
|
||
|
# BOTTOM SIDE PROPERTY
|
||
|
@property
|
||
|
def bottom(self):
|
||
|
"""The y coordinate for the bottom edge of the rectangle.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.bottom
|
||
|
20
|
||
|
>>> r.bottom = 30
|
||
|
>>> r
|
||
|
Rect(left=0, top=10, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(BOTTOM)
|
||
|
return self._top + self._height
|
||
|
|
||
|
@bottom.setter
|
||
|
def bottom(self, newBottom):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForIntOrFloat(newBottom)
|
||
|
if newBottom != self._top + self._height: # Only run this code if the size/position has changed.
|
||
|
originalTop = self._top
|
||
|
if self._enableFloat:
|
||
|
self._top = newBottom - self._height
|
||
|
else:
|
||
|
self._top = int(newBottom) - self._height
|
||
|
self.callOnChange(self._left, originalTop, self._width, self._height)
|
||
|
|
||
|
|
||
|
# TOP LEFT CORNER PROPERTY
|
||
|
@property
|
||
|
def topleft(self):
|
||
|
"""
|
||
|
The x and y coordinates for the top right corner of the rectangle, as a tuple.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.topleft
|
||
|
(0, 0)
|
||
|
>>> r.topleft = (30, 30)
|
||
|
>>> r
|
||
|
Rect(left=30, top=30, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(TOPLEFT)
|
||
|
return Point(x=self._left, y=self._top)
|
||
|
|
||
|
@topleft.setter
|
||
|
def topleft(self, value):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForTwoIntOrFloatTuple(value)
|
||
|
newLeft, newTop = value
|
||
|
if (newLeft != self._left) or (newTop != self._top): # Only run this code if the size/position has changed.
|
||
|
originalLeft = self._left
|
||
|
originalTop = self._top
|
||
|
if self._enableFloat:
|
||
|
self._left = newLeft
|
||
|
self._top = newTop
|
||
|
else:
|
||
|
self._left = int(newLeft)
|
||
|
self._top = int(newTop)
|
||
|
self.callOnChange(originalLeft, originalTop, self._width, self._height)
|
||
|
|
||
|
|
||
|
# BOTTOM LEFT CORNER PROPERTY
|
||
|
@property
|
||
|
def bottomleft(self):
|
||
|
"""
|
||
|
The x and y coordinates for the bottom right corner of the rectangle, as a tuple.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.bottomleft
|
||
|
(0, 20)
|
||
|
>>> r.bottomleft = (30, 30)
|
||
|
>>> r
|
||
|
Rect(left=30, top=10, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(BOTTOMLEFT)
|
||
|
return Point(x=self._left, y=self._top + self._height)
|
||
|
|
||
|
@bottomleft.setter
|
||
|
def bottomleft(self, value):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForTwoIntOrFloatTuple(value)
|
||
|
newLeft, newBottom = value
|
||
|
if (newLeft != self._left) or (newBottom != self._top + self._height): # Only run this code if the size/position has changed.
|
||
|
originalLeft = self._left
|
||
|
originalTop = self._top
|
||
|
if self._enableFloat:
|
||
|
self._left = newLeft
|
||
|
self._top = newBottom - self._height
|
||
|
else:
|
||
|
self._left = int(newLeft)
|
||
|
self._top = int(newBottom) - self._height
|
||
|
self.callOnChange(originalLeft, originalTop, self._width, self._height)
|
||
|
|
||
|
|
||
|
# TOP RIGHT CORNER PROPERTY
|
||
|
@property
|
||
|
def topright(self):
|
||
|
"""
|
||
|
The x and y coordinates for the top right corner of the rectangle, as a tuple.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.topright
|
||
|
(10, 0)
|
||
|
>>> r.topright = (30, 30)
|
||
|
>>> r
|
||
|
Rect(left=20, top=30, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(TOPRIGHT)
|
||
|
return Point(x=self._left + self._width, y=self._top)
|
||
|
|
||
|
@topright.setter
|
||
|
def topright(self, value):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForTwoIntOrFloatTuple(value)
|
||
|
newRight, newTop = value
|
||
|
if (newRight != self._left + self._width) or (newTop != self._top): # Only run this code if the size/position has changed.
|
||
|
originalLeft = self._left
|
||
|
originalTop = self._top
|
||
|
if self._enableFloat:
|
||
|
self._left = newRight - self._width
|
||
|
self._top = newTop
|
||
|
else:
|
||
|
self._left = int(newRight) - self._width
|
||
|
self._top = int(newTop)
|
||
|
self.callOnChange(originalLeft, originalTop, self._width, self._height)
|
||
|
|
||
|
|
||
|
# BOTTOM RIGHT CORNER PROPERTY
|
||
|
@property
|
||
|
def bottomright(self):
|
||
|
"""
|
||
|
The x and y coordinates for the bottom right corner of the rectangle, as a tuple.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.bottomright
|
||
|
(10, 20)
|
||
|
>>> r.bottomright = (30, 30)
|
||
|
>>> r
|
||
|
Rect(left=20, top=10, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(BOTTOMRIGHT)
|
||
|
return Point(x=self._left + self._width, y=self._top + self._height)
|
||
|
|
||
|
@bottomright.setter
|
||
|
def bottomright(self, value):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForTwoIntOrFloatTuple(value)
|
||
|
newRight, newBottom = value
|
||
|
if (newBottom != self._top + self._height) or (newRight != self._left + self._width): # Only run this code if the size/position has changed.
|
||
|
originalLeft = self._left
|
||
|
originalTop = self._top
|
||
|
if self._enableFloat:
|
||
|
self._left = newRight - self._width
|
||
|
self._top = newBottom - self._height
|
||
|
else:
|
||
|
self._left = int(newRight) - self._width
|
||
|
self._top = int(newBottom) - self._height
|
||
|
self.callOnChange(originalLeft, originalTop, self._width, self._height)
|
||
|
|
||
|
|
||
|
# MIDDLE OF TOP SIDE PROPERTY
|
||
|
@property
|
||
|
def midtop(self):
|
||
|
"""
|
||
|
The x and y coordinates for the midpoint of the top edge of the rectangle, as a tuple.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.midtop
|
||
|
(5, 0)
|
||
|
>>> r.midtop = (40, 50)
|
||
|
>>> r
|
||
|
Rect(left=35, top=50, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(MIDTOP)
|
||
|
if self._enableFloat:
|
||
|
return Point(x=self._left + (self._width / 2.0), y=self._top)
|
||
|
else:
|
||
|
return Point(x=self._left + (self._width // 2), y=self._top)
|
||
|
|
||
|
@midtop.setter
|
||
|
def midtop(self, value):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForTwoIntOrFloatTuple(value)
|
||
|
newMidTop, newTop = value
|
||
|
originalLeft = self._left
|
||
|
originalTop = self._top
|
||
|
if self._enableFloat:
|
||
|
if (newMidTop != self._left + self._width / 2.0) or (newTop != self._top): # Only run this code if the size/position has changed.
|
||
|
self._left = newMidTop - (self._width / 2.0)
|
||
|
self._top = newTop
|
||
|
self.callOnChange(originalLeft, originalTop, self._width, self._height)
|
||
|
else:
|
||
|
if (newMidTop != self._left + self._width // 2) or (newTop != self._top): # Only run this code if the size/position has changed.
|
||
|
self._left = int(newMidTop) - (self._width // 2)
|
||
|
self._top = int(newTop)
|
||
|
self.callOnChange(originalLeft, originalTop, self._width, self._height)
|
||
|
|
||
|
|
||
|
# MIDDLE OF BOTTOM SIDE PROPERTY
|
||
|
@property
|
||
|
def midbottom(self):
|
||
|
"""
|
||
|
The x and y coordinates for the midpoint of the bottom edge of the rectangle, as a tuple.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.midbottom
|
||
|
(5, 20)
|
||
|
>>> r.midbottom = (40, 50)
|
||
|
>>> r
|
||
|
Rect(left=35, top=30, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(MIDBOTTOM)
|
||
|
if self._enableFloat:
|
||
|
return Point(x=self._left + (self._width / 2.0), y=self._top + self._height)
|
||
|
else:
|
||
|
return Point(x=self._left + (self._width // 2), y=self._top + self._height)
|
||
|
|
||
|
@midbottom.setter
|
||
|
def midbottom(self, value):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForTwoIntOrFloatTuple(value)
|
||
|
newMidBottom, newBottom = value
|
||
|
originalLeft = self._left
|
||
|
originalTop = self._top
|
||
|
if self._enableFloat:
|
||
|
if (newMidBottom != self._left + self._width / 2.0) or (newBottom != self._top + self._height): # Only run this code if the size/position has changed.
|
||
|
self._left = newMidBottom - (self._width / 2.0)
|
||
|
self._top = newBottom - self._height
|
||
|
self.callOnChange(originalLeft, originalTop, self._width, self._height)
|
||
|
else:
|
||
|
if (newMidBottom != self._left + self._width // 2) or (newBottom != self._top + self._height): # Only run this code if the size/position has changed.
|
||
|
self._left = int(newMidBottom) - (self._width // 2)
|
||
|
self._top = int(newBottom) - self._height
|
||
|
self.callOnChange(originalLeft, originalTop, self._width, self._height)
|
||
|
|
||
|
|
||
|
# MIDDLE OF LEFT SIDE PROPERTY
|
||
|
@property
|
||
|
def midleft(self):
|
||
|
"""
|
||
|
The x and y coordinates for the midpoint of the left edge of the rectangle, as a tuple.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.midleft
|
||
|
(0, 10)
|
||
|
>>> r.midleft = (40, 50)
|
||
|
>>> r
|
||
|
Rect(left=40, top=40, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(MIDLEFT)
|
||
|
if self._enableFloat:
|
||
|
return Point(x=self._left, y=self._top + (self._height / 2.0))
|
||
|
else:
|
||
|
return Point(x=self._left, y=self._top + (self._height // 2))
|
||
|
|
||
|
|
||
|
@midleft.setter
|
||
|
def midleft(self, value):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForTwoIntOrFloatTuple(value)
|
||
|
newLeft, newMidLeft = value
|
||
|
originalLeft = self._left
|
||
|
originalTop = self._top
|
||
|
if self._enableFloat:
|
||
|
if (newLeft != self._left) or (newMidLeft != self._top + (self._height / 2.0)): # Only run this code if the size/position has changed.
|
||
|
self._left = newLeft
|
||
|
self._top = newMidLeft - (self._height / 2.0)
|
||
|
self.callOnChange(originalLeft, originalTop, self._width, self._height)
|
||
|
else:
|
||
|
if (newLeft != self._left) or (newMidLeft != self._top + (self._height // 2)): # Only run this code if the size/position has changed.
|
||
|
self._left = int(newLeft)
|
||
|
self._top = int(newMidLeft) - (self._height // 2)
|
||
|
self.callOnChange(originalLeft, originalTop, self._width, self._height)
|
||
|
|
||
|
|
||
|
# MIDDLE OF RIGHT SIDE PROPERTY
|
||
|
@property
|
||
|
def midright(self):
|
||
|
"""
|
||
|
The x and y coordinates for the midpoint of the right edge of the rectangle, as a tuple.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.midright
|
||
|
(10, 10)
|
||
|
>>> r.midright = (40, 50)
|
||
|
>>> r
|
||
|
Rect(left=30, top=40, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(MIDRIGHT)
|
||
|
if self._enableFloat:
|
||
|
return Point(x=self._left + self._width, y=self._top + (self._height / 2.0))
|
||
|
else:
|
||
|
return Point(x=self._left + self._width, y=self._top + (self._height // 2))
|
||
|
|
||
|
|
||
|
@midright.setter
|
||
|
def midright(self, value):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForTwoIntOrFloatTuple(value)
|
||
|
newRight, newMidRight = value
|
||
|
originalLeft = self._left
|
||
|
originalTop = self._top
|
||
|
if self._enableFloat:
|
||
|
if (newRight != self._left + self._width) or (newMidRight != self._top + self._height / 2.0): # Only run this code if the size/position has changed.
|
||
|
self._left = newRight - self._width
|
||
|
self._top = newMidRight - (self._height / 2.0)
|
||
|
self.callOnChange(originalLeft, originalTop, self._width, self._height)
|
||
|
else:
|
||
|
if (newRight != self._left + self._width) or (newMidRight != self._top + self._height // 2): # Only run this code if the size/position has changed.
|
||
|
self._left = int(newRight) - self._width
|
||
|
self._top = int(newMidRight) - (self._height // 2)
|
||
|
self.callOnChange(originalLeft, originalTop, self._width, self._height)
|
||
|
|
||
|
|
||
|
# CENTER POINT PROPERTY
|
||
|
@property
|
||
|
def center(self):
|
||
|
"""
|
||
|
The x and y coordinates for the center of the rectangle, as a tuple.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.center
|
||
|
(5, 10)
|
||
|
>>> r.center = (40, 50)
|
||
|
>>> r
|
||
|
Rect(left=35, top=40, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(CENTER)
|
||
|
if self._enableFloat:
|
||
|
return Point(x=self._left + (self._width / 2.0), y=self._top + (self._height / 2.0))
|
||
|
else:
|
||
|
return Point(x=self._left + (self._width // 2), y=self._top + (self._height // 2))
|
||
|
|
||
|
@center.setter
|
||
|
def center(self, value):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForTwoIntOrFloatTuple(value)
|
||
|
newCenterx, newCentery = value
|
||
|
originalLeft = self._left
|
||
|
originalTop = self._top
|
||
|
if self._enableFloat:
|
||
|
if (newCenterx != self._left + self._width / 2.0) or (newCentery != self._top + self._height / 2.0): # Only run this code if the size/position has changed.
|
||
|
self._left = newCenterx - (self._width / 2.0)
|
||
|
self._top = newCentery - (self._height / 2.0)
|
||
|
self.callOnChange(originalLeft, originalTop, self._width, self._height)
|
||
|
else:
|
||
|
if (newCenterx != self._left + self._width // 2) or (newCentery != self._top + self._height // 2): # Only run this code if the size/position has changed.
|
||
|
self._left = int(newCenterx) - (self._width // 2)
|
||
|
self._top = int(newCentery) - (self._height // 2)
|
||
|
self.callOnChange(originalLeft, originalTop, self._width, self._height)
|
||
|
|
||
|
|
||
|
# X COORDINATE OF CENTER POINT PROPERTY
|
||
|
@property
|
||
|
def centerx(self):
|
||
|
"""
|
||
|
The x coordinate for the center of the rectangle, as a tuple.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.centerx
|
||
|
5
|
||
|
>>> r.centerx = 50
|
||
|
>>> r
|
||
|
Rect(left=45, top=0, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(CENTERX)
|
||
|
if self._enableFloat:
|
||
|
return self._left + (self._width / 2.0)
|
||
|
else:
|
||
|
return self._left + (self._width // 2)
|
||
|
|
||
|
@centerx.setter
|
||
|
def centerx(self, newCenterx):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForIntOrFloat(newCenterx)
|
||
|
originalLeft = self._left
|
||
|
if self._enableFloat:
|
||
|
if (newCenterx != self._left + self._width / 2.0): # Only run this code if the size/position has changed.
|
||
|
self._left = newCenterx - (self._width / 2.0)
|
||
|
self.callOnChange(originalLeft, self._top, self._width, self._height)
|
||
|
else:
|
||
|
if (newCenterx != self._left + self._width // 2): # Only run this code if the size/position has changed.
|
||
|
self._left = int(newCenterx) - (self._width // 2)
|
||
|
self.callOnChange(originalLeft, self._top, self._width, self._height)
|
||
|
|
||
|
|
||
|
# Y COORDINATE OF CENTER POINT PROPERTY
|
||
|
@property
|
||
|
def centery(self):
|
||
|
"""
|
||
|
The y coordinate for the center of the rectangle, as a tuple.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.centery
|
||
|
10
|
||
|
>>> r.centery = 50
|
||
|
>>> r
|
||
|
Rect(left=0, top=40, width=10, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(CENTERY)
|
||
|
if self._enableFloat:
|
||
|
return self._top + (self._height / 2.0)
|
||
|
else:
|
||
|
return self._top + (self._height // 2)
|
||
|
|
||
|
@centery.setter
|
||
|
def centery(self, newCentery):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForIntOrFloat(newCentery)
|
||
|
originalTop = self._top
|
||
|
if self._enableFloat:
|
||
|
if (newCentery != self._top + self._height / 2.0): # Only run this code if the size/position has changed.
|
||
|
self._top = newCentery - (self._height / 2.0)
|
||
|
self.callOnChange(self._left, originalTop, self._width, self._height)
|
||
|
else:
|
||
|
if (newCentery != self._top + self._height // 2): # Only run this code if the size/position has changed.
|
||
|
self._top = int(newCentery) - (self._height // 2)
|
||
|
self.callOnChange(self._left, originalTop, self._width, self._height)
|
||
|
|
||
|
|
||
|
# SIZE PROPERTY (i.e. (width, height))
|
||
|
@property
|
||
|
def size(self):
|
||
|
"""
|
||
|
The width and height of the rectangle, as a tuple.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.size
|
||
|
(10, 20)
|
||
|
>>> r.size = (40, 50)
|
||
|
>>> r
|
||
|
Rect(left=0, top=0, width=40, height=50)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(SIZE)
|
||
|
return Size(width=self._width, height=self._height)
|
||
|
|
||
|
@size.setter
|
||
|
def size(self, value):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForTwoIntOrFloatTuple(value)
|
||
|
newWidth, newHeight = value
|
||
|
if newWidth != self._width or newHeight != self._height:
|
||
|
originalWidth = self._width
|
||
|
originalHeight = self._height
|
||
|
if self._enableFloat:
|
||
|
self._width = newWidth
|
||
|
self._height = newHeight
|
||
|
else:
|
||
|
self._width = int(newWidth)
|
||
|
self._height = int(newHeight)
|
||
|
self.callOnChange(self._left, self._top, originalWidth, originalHeight)
|
||
|
|
||
|
|
||
|
# WIDTH PROPERTY
|
||
|
@property
|
||
|
def width(self):
|
||
|
"""
|
||
|
The width of the rectangle. `w` is an alias for `width`.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.width
|
||
|
10
|
||
|
>>> r.width = 50
|
||
|
>>> r
|
||
|
Rect(left=0, top=0, width=50, height=20)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(WIDTH)
|
||
|
return self._width
|
||
|
|
||
|
@width.setter
|
||
|
def width(self, newWidth):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForIntOrFloat(newWidth)
|
||
|
if (newWidth != self._width): # Only run this code if the size/position has changed.
|
||
|
originalWidth = self._width
|
||
|
if self._enableFloat:
|
||
|
self._width = newWidth
|
||
|
else:
|
||
|
self._width = int(newWidth)
|
||
|
self.callOnChange(self._left, self._top, originalWidth, self._height)
|
||
|
|
||
|
w = width
|
||
|
|
||
|
|
||
|
# HEIGHT PROPERTY
|
||
|
@property
|
||
|
def height(self):
|
||
|
"""
|
||
|
The height of the rectangle. `h` is an alias for `height`
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.height
|
||
|
20
|
||
|
>>> r.height = 50
|
||
|
>>> r
|
||
|
Rect(left=0, top=0, width=10, height=50)
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(HEIGHT)
|
||
|
return self._height
|
||
|
|
||
|
@height.setter
|
||
|
def height(self, newHeight):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForIntOrFloat(newHeight)
|
||
|
if (newHeight != self._height): # Only run this code if the size/position has changed.
|
||
|
originalHeight = self._height
|
||
|
if self._enableFloat:
|
||
|
self._height = newHeight
|
||
|
else:
|
||
|
self._height = int(newHeight)
|
||
|
self.callOnChange(self._left, self._top, self._width, originalHeight)
|
||
|
|
||
|
h = height
|
||
|
|
||
|
|
||
|
# AREA PROPERTY
|
||
|
@property
|
||
|
def area(self):
|
||
|
"""The area of the `Rect`, which is simply the width times the height.
|
||
|
This is a read-only attribute.
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.area
|
||
|
200
|
||
|
"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(AREA)
|
||
|
return self._width * self._height
|
||
|
|
||
|
|
||
|
# BOX PROPERTY
|
||
|
@property
|
||
|
def box(self):
|
||
|
"""A tuple of four integers: (left, top, width, height).
|
||
|
|
||
|
>>> r = Rect(0, 0, 10, 20)
|
||
|
>>> r.box
|
||
|
(0, 0, 10, 20)
|
||
|
>>> r.box = (5, 15, 100, 200)
|
||
|
>>> r.box
|
||
|
(5, 15, 100, 200)"""
|
||
|
if self.onRead is not None:
|
||
|
self.onRead(BOX)
|
||
|
return Box(left=self._left, top=self._top, width=self._width, height=self._height)
|
||
|
|
||
|
@box.setter
|
||
|
def box(self, value):
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForFourIntOrFloatTuple(value)
|
||
|
newLeft, newTop, newWidth, newHeight = value
|
||
|
if (newLeft != self._left) or (newTop != self._top) or (newWidth != self._width) or (newHeight != self._height):
|
||
|
originalLeft = self._left
|
||
|
originalTop = self._top
|
||
|
originalWidth = self._width
|
||
|
originalHeight = self._height
|
||
|
if self._enableFloat:
|
||
|
self._left = float(newLeft)
|
||
|
self._top = float(newTop)
|
||
|
self._width = float(newWidth)
|
||
|
self._height = float(newHeight)
|
||
|
else:
|
||
|
self._left = int(newLeft)
|
||
|
self._top = int(newTop)
|
||
|
self._width = int(newWidth)
|
||
|
self._height = int(newHeight)
|
||
|
self.callOnChange(originalLeft, originalTop, originalWidth, originalHeight)
|
||
|
|
||
|
|
||
|
def get(self, rectAttrName):
|
||
|
# Access via the properties so that it triggers onRead().
|
||
|
if rectAttrName == TOP:
|
||
|
return self.top
|
||
|
elif rectAttrName == BOTTOM:
|
||
|
return self.bottom
|
||
|
elif rectAttrName == LEFT:
|
||
|
return self.left
|
||
|
elif rectAttrName == RIGHT:
|
||
|
return self.right
|
||
|
elif rectAttrName == TOPLEFT:
|
||
|
return self.topleft
|
||
|
elif rectAttrName == TOPRIGHT:
|
||
|
return self.topright
|
||
|
elif rectAttrName == BOTTOMLEFT:
|
||
|
return self.bottomleft
|
||
|
elif rectAttrName == BOTTOMRIGHT:
|
||
|
return self.bottomright
|
||
|
elif rectAttrName == MIDTOP:
|
||
|
return self.midtop
|
||
|
elif rectAttrName == MIDBOTTOM:
|
||
|
return self.midbottom
|
||
|
elif rectAttrName == MIDLEFT:
|
||
|
return self.midleft
|
||
|
elif rectAttrName == MIDRIGHT:
|
||
|
return self.midright
|
||
|
elif rectAttrName == CENTER:
|
||
|
return self.center
|
||
|
elif rectAttrName == CENTERX:
|
||
|
return self.centerx
|
||
|
elif rectAttrName == CENTERY:
|
||
|
return self.centery
|
||
|
elif rectAttrName == WIDTH:
|
||
|
return self.width
|
||
|
elif rectAttrName == HEIGHT:
|
||
|
return self.height
|
||
|
elif rectAttrName == SIZE:
|
||
|
return self.size
|
||
|
elif rectAttrName == AREA:
|
||
|
return self.area
|
||
|
elif rectAttrName == BOX:
|
||
|
return self.box
|
||
|
else:
|
||
|
raise PyRectException("'%s' is not a valid attribute name" % (rectAttrName))
|
||
|
|
||
|
|
||
|
def set(self, rectAttrName, value):
|
||
|
# Set via the properties so that it triggers onChange().
|
||
|
if rectAttrName == TOP:
|
||
|
self.top = value
|
||
|
elif rectAttrName == BOTTOM:
|
||
|
self.bottom = value
|
||
|
elif rectAttrName == LEFT:
|
||
|
self.left = value
|
||
|
elif rectAttrName == RIGHT:
|
||
|
self.right = value
|
||
|
elif rectAttrName == TOPLEFT:
|
||
|
self.topleft = value
|
||
|
elif rectAttrName == TOPRIGHT:
|
||
|
self.topright = value
|
||
|
elif rectAttrName == BOTTOMLEFT:
|
||
|
self.bottomleft = value
|
||
|
elif rectAttrName == BOTTOMRIGHT:
|
||
|
self.bottomright = value
|
||
|
elif rectAttrName == MIDTOP:
|
||
|
self.midtop = value
|
||
|
elif rectAttrName == MIDBOTTOM:
|
||
|
self.midbottom = value
|
||
|
elif rectAttrName == MIDLEFT:
|
||
|
self.midleft = value
|
||
|
elif rectAttrName == MIDRIGHT:
|
||
|
self.midright = value
|
||
|
elif rectAttrName == CENTER:
|
||
|
self.center = value
|
||
|
elif rectAttrName == CENTERX:
|
||
|
self.centerx = value
|
||
|
elif rectAttrName == CENTERY:
|
||
|
self.centery = value
|
||
|
elif rectAttrName == WIDTH:
|
||
|
self.width = value
|
||
|
elif rectAttrName == HEIGHT:
|
||
|
self.height = value
|
||
|
elif rectAttrName == SIZE:
|
||
|
self.size = value
|
||
|
elif rectAttrName == AREA:
|
||
|
raise PyRectException('area is a read-only attribute')
|
||
|
elif rectAttrName == BOX:
|
||
|
self.box = value
|
||
|
else:
|
||
|
raise PyRectException("'%s' is not a valid attribute name" % (rectAttrName))
|
||
|
|
||
|
|
||
|
def move(self, xOffset, yOffset):
|
||
|
"""Moves this Rect object by the given offsets. The xOffset and yOffset
|
||
|
arguments can be any integer value, positive or negative.
|
||
|
>>> r = Rect(0, 0, 100, 100)
|
||
|
>>> r.move(10, 20)
|
||
|
>>> r
|
||
|
Rect(left=10, top=20, width=100, height=100)
|
||
|
"""
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
_checkForIntOrFloat(xOffset)
|
||
|
_checkForIntOrFloat(yOffset)
|
||
|
if self._enableFloat:
|
||
|
self._left += xOffset
|
||
|
self._top += yOffset
|
||
|
else:
|
||
|
self._left += int(xOffset)
|
||
|
self._top += int(yOffset)
|
||
|
|
||
|
|
||
|
def copy(self):
|
||
|
"""Return a copied `Rect` object with the same position and size as this
|
||
|
`Rect` object.
|
||
|
|
||
|
>>> r1 = Rect(0, 0, 100, 150)
|
||
|
>>> r2 = r1.copy()
|
||
|
>>> r1 == r2
|
||
|
True
|
||
|
>>> r2
|
||
|
Rect(left=0, top=0, width=100, height=150)
|
||
|
"""
|
||
|
return Rect(self._left, self._top, self._width, self._height, self._enableFloat, self._readOnly)
|
||
|
|
||
|
|
||
|
def inflate(self, widthChange=0, heightChange=0):
|
||
|
"""Increases the size of this Rect object by the given offsets. The
|
||
|
rectangle's center doesn't move. Negative values will shrink the
|
||
|
rectangle.
|
||
|
|
||
|
>>> r = Rect(0, 0, 100, 150)
|
||
|
>>> r.inflate(20, 40)
|
||
|
>>> r
|
||
|
Rect(left=-10, top=-20, width=120, height=190)
|
||
|
"""
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
originalCenter = self.center
|
||
|
self.width += widthChange
|
||
|
self.height += heightChange
|
||
|
self.center = originalCenter
|
||
|
|
||
|
|
||
|
def clamp(self, otherRect):
|
||
|
"""Centers this Rect object at the center of otherRect.
|
||
|
|
||
|
>>> r1 =Rect(0, 0, 100, 100)
|
||
|
>>> r2 = Rect(-20, -90, 50, 50)
|
||
|
>>> r2.clamp(r1)
|
||
|
>>> r2
|
||
|
Rect(left=25, top=25, width=50, height=50)
|
||
|
>>> r1.center == r2.center
|
||
|
True
|
||
|
"""
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
self.center = otherRect.center
|
||
|
|
||
|
'''
|
||
|
def intersection(self, otherRect):
|
||
|
"""Returns a new Rect object of the overlapping area between this
|
||
|
Rect object and otherRect.
|
||
|
|
||
|
`clip()` is an alias for `intersection()`.
|
||
|
"""
|
||
|
pass
|
||
|
|
||
|
clip = intersection
|
||
|
'''
|
||
|
|
||
|
def union(self, otherRect):
|
||
|
"""Adjusts the width and height to also cover the area of `otherRect`.
|
||
|
|
||
|
>>> r1 = Rect(0, 0, 100, 100)
|
||
|
>>> r2 = Rect(-10, -10, 100, 100)
|
||
|
>>> r1.union(r2)
|
||
|
>>> r1
|
||
|
Rect(left=-10, top=-10, width=110, height=110)
|
||
|
"""
|
||
|
|
||
|
# TODO - Change otherRect so that it could be a point as well.
|
||
|
|
||
|
unionLeft = min(self._left, otherRect._left)
|
||
|
unionTop = min(self._top, otherRect._top)
|
||
|
unionRight = max(self.right, otherRect.right)
|
||
|
unionBottom = max(self.bottom, otherRect.bottom)
|
||
|
|
||
|
self._left = unionLeft
|
||
|
self._top = unionTop
|
||
|
self._width = unionRight - unionLeft
|
||
|
self._height = unionBottom - unionTop
|
||
|
|
||
|
|
||
|
def unionAll(self, otherRects):
|
||
|
"""Adjusts the width and height to also cover all the `Rect` objects in
|
||
|
the `otherRects` sequence.
|
||
|
|
||
|
>>> r = Rect(0, 0, 100, 100)
|
||
|
>>> r1 = Rect(0, 0, 150, 100)
|
||
|
>>> r2 = Rect(-10, -10, 100, 100)
|
||
|
>>> r.unionAll([r1, r2])
|
||
|
>>> r
|
||
|
Rect(left=-10, top=-10, width=160, height=110)
|
||
|
"""
|
||
|
|
||
|
# TODO - Change otherRect so that it could be a point as well.
|
||
|
|
||
|
otherRects = list(otherRects)
|
||
|
otherRects.append(self)
|
||
|
|
||
|
unionLeft = min([r._left for r in otherRects])
|
||
|
unionTop = min([r._top for r in otherRects])
|
||
|
unionRight = max([r.right for r in otherRects])
|
||
|
unionBottom = max([r.bottom for r in otherRects])
|
||
|
|
||
|
self._left = unionLeft
|
||
|
self._top = unionTop
|
||
|
self._width = unionRight - unionLeft
|
||
|
self._height = unionBottom - unionTop
|
||
|
|
||
|
"""
|
||
|
def fit(self, other):
|
||
|
pass # TODO - needs to be complete
|
||
|
"""
|
||
|
|
||
|
|
||
|
def normalize(self):
|
||
|
"""Rect objects with a negative width or height cover a region where the
|
||
|
right/bottom edge is to the left/above of the left/top edge, respectively.
|
||
|
The `normalize()` method sets the `width` and `height` to positive if they
|
||
|
were negative.
|
||
|
|
||
|
The Rect stays in the same place, though with the `top` and `left`
|
||
|
attributes representing the true top and left side.
|
||
|
|
||
|
>>> r = Rect(0, 0, -10, -20)
|
||
|
>>> r.normalize()
|
||
|
>>> r
|
||
|
Rect(left=-10, top=-20, width=10, height=20)
|
||
|
"""
|
||
|
if self._readOnly:
|
||
|
raise PyRectException('Rect object is read-only')
|
||
|
|
||
|
if self._width < 0:
|
||
|
self._width = -self._width
|
||
|
self._left -= self._width
|
||
|
if self._height < 0:
|
||
|
self._height = -self._height
|
||
|
self._top -= self._height
|
||
|
# Note: No need to intify here, since the four attributes should already be ints and no multiplication was done.
|
||
|
|
||
|
|
||
|
def __contains__(self, value): # for either points or other Rect objects. For Rects, the *entire* Rect must be in this Rect.
|
||
|
if isinstance(value, Rect):
|
||
|
return value.topleft in self and value.topright in self and value.bottomleft in self and value.bottomright in self
|
||
|
|
||
|
# Check if value is an (x, y) sequence or a (left, top, width, height) sequence.
|
||
|
try:
|
||
|
len(value)
|
||
|
except:
|
||
|
raise PyRectException('in <Rect> requires an (x, y) tuple, a (left, top, width, height) tuple, or a Rect object as left operand, not %s' % (value.__class__.__name__))
|
||
|
|
||
|
if len(value) == 2:
|
||
|
# Assume that value is an (x, y) sequence.
|
||
|
_checkForTwoIntOrFloatTuple(value)
|
||
|
x, y = value
|
||
|
return self._left < x < self._left + self._width and self._top < y < self._top + self._height
|
||
|
|
||
|
elif len(value) == 4:
|
||
|
# Assume that value is an (x, y) sequence.
|
||
|
_checkForFourIntOrFloatTuple(value)
|
||
|
left, top, width, height = value
|
||
|
return (left, top) in self and (left + width, top) in self and (left, top + height) in self and (left + width, top + height) in self
|
||
|
else:
|
||
|
raise PyRectException('in <Rect> requires an (x, y) tuple, a (left, top, width, height) tuple, or a Rect object as left operand, not %s' % (value.__class__.__name__))
|
||
|
|
||
|
|
||
|
def collide(self, value):
|
||
|
"""Returns `True` if value collides with this `Rect` object, where value can
|
||
|
be an (x, y) tuple, a (left, top, width, height) box tuple, or another `Rect`
|
||
|
object. If value represents a rectangular area, any part of that area
|
||
|
can collide with this `Rect` object to make `collide()` return `True`.
|
||
|
Otherwise, returns `False`."""
|
||
|
|
||
|
# Note: This code is similar to __contains__(), with some minor changes
|
||
|
# because __contains__() requires the rectangular are to be COMPELTELY
|
||
|
# within the Rect object.
|
||
|
if isinstance(value, Rect):
|
||
|
return value.topleft in self or value.topright in self or value.bottomleft in self or value.bottomright in self
|
||
|
|
||
|
# Check if value is an (x, y) sequence or a (left, top, width, height) sequence.
|
||
|
try:
|
||
|
len(value)
|
||
|
except:
|
||
|
raise PyRectException('in <Rect> requires an (x, y) tuple, a (left, top, width, height) tuple, or a Rect object as left operand, not %s' % (value.__class__.__name__))
|
||
|
|
||
|
if len(value) == 2:
|
||
|
# Assume that value is an (x, y) sequence.
|
||
|
_checkForTwoIntOrFloatTuple(value)
|
||
|
x, y = value
|
||
|
return self._left < x < self._left + self._width and self._top < y < self._top + self._height
|
||
|
|
||
|
elif len(value) == 4:
|
||
|
# Assume that value is an (x, y) sequence.
|
||
|
left, top, width, height = value
|
||
|
return (left, top) in self or (left + width, top) in self or (left, top + height) in self or (left + width, top + height) in self
|
||
|
else:
|
||
|
raise PyRectException('in <Rect> requires an (x, y) tuple, a (left, top, width, height) tuple, or a Rect object as left operand, not %s' % (value.__class__.__name__))
|
||
|
|
||
|
|
||
|
'''
|
||
|
def collideAny(self, rectsOrPoints):
|
||
|
"""Returns True if any of the (x, y) or (left, top, width, height)
|
||
|
tuples in rectsOrPoints is inside this Rect object.
|
||
|
|
||
|
>> r = Rect(0, 0, 100, 100)
|
||
|
>> p1 = (150, 80)
|
||
|
>> p2 = (100, 100) # This point collides.
|
||
|
>> r.collideAny([p1, p2])
|
||
|
True
|
||
|
>> r1 = Rect(50, 50, 10, 20) # This Rect collides.
|
||
|
>> r.collideAny([r1])
|
||
|
True
|
||
|
>> r.collideAny([p1, p2, r1])
|
||
|
True
|
||
|
"""
|
||
|
# TODO - needs to be complete
|
||
|
pass # returns True or False
|
||
|
raise NotImplementedError
|
||
|
'''
|
||
|
|
||
|
'''
|
||
|
def collideAll(self, rectsOrPoints):
|
||
|
"""Returns True if all of the (x, y) or (left, top, width, height)
|
||
|
tuples in rectsOrPoints is inside this Rect object.
|
||
|
"""
|
||
|
|
||
|
pass # return a list of all rects or points that collide with any other in the argument
|
||
|
raise NotImplementedError
|
||
|
'''
|
||
|
|
||
|
# TODO - Add overloaded operators for + - * / and others once we can determine actual use cases for them.
|
||
|
|
||
|
"""NOTE: All of the comparison magic methods compare the box tuple of Rect
|
||
|
objects. This is the behavior of the pygame Rect objects. Originally,
|
||
|
I thought about having the <, <=, >, and >= operators compare the area
|
||
|
of Rect objects. But at the same time, I wanted to have == and != compare
|
||
|
not just area, but all four left, top, width, and height attributes.
|
||
|
But that's weird to have different comparison operators comparing different
|
||
|
features of a rectangular area. So I just defaulted to what Pygame does
|
||
|
and compares the box tuple. This means that the == and != operators are
|
||
|
the only really useful comparison operators, so I decided to ditch the
|
||
|
other operators altogether and just have Rect only support == and !=.
|
||
|
"""
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
if isinstance(other, Rect):
|
||
|
return other.box == self.box
|
||
|
else:
|
||
|
raise PyRectException('Rect objects can only be compared with other Rect objects')
|
||
|
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
if isinstance(other, Rect):
|
||
|
return other.box != self.box
|
||
|
else:
|
||
|
raise PyRectException('Rect objects can only be compared with other Rect objects')
|
||
|
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
print(doctest.testmod())
|
||
|
|