Searching the internet has not given a satisfactory solution for the following problem. Given a class Rectangle
defined as the following:
class Rectangle:
def __init__(self, x1, y1, x2, y2):
if x1 > x2 or y1 > y2:
raise ValueError('coordinates are invalid')
self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
@property
def width(self):
return self.x2 - self.x1
@property
def height(self):
return self.y2 - self.y1
def bounds(self, other):
return Rectangle(min(self.x1, other.x1), min(self.y1, other.y1),
max(self.x2, other.x2), max(self.y2, other.y2))
def intersect(self, other):
return self.x1 < other.x2 and self.x2 > other.x1 and \
self.y1 < other.y2 and self.y2 > other.y1
How would you create a method to get the intersection and a generator to get the difference of two rectangles? Presumably, a more complete implementation of the following methods are needed, but it is not clear to me what should be written.
def __and__(self, other):
if self.intersect(other):
# return a new rectangle that provides
# the intersection between self and other
return None
def __sub__(self, other):
take_away = self & other
if take_away is None:
return self
if take_away is self:
return None
return self.get_partitions(take_away)
def get_partitions(self, take_away):
# yield 1 or 3 rectangles that are not part of take_away
# alternative:
# yield 1 or 2 overlapping rectangles not part of take_away
Does anyone have an elegant implementation for these methods? My hope is to avoid writing code for every conceivable case that might be encountered.
Here is a complete solution for you.
Methods in the class are ordered illogically so that the important parts are visible without scrolling.
import itertools
class Rectangle:
def intersection(self, other):
a, b = self, other
x1 = max(min(a.x1, a.x2), min(b.x1, b.x2))
y1 = max(min(a.y1, a.y2), min(b.y1, b.y2))
x2 = min(max(a.x1, a.x2), max(b.x1, b.x2))
y2 = min(max(a.y1, a.y2), max(b.y1, b.y2))
if x1<x2 and y1<y2:
return type(self)(x1, y1, x2, y2)
__and__ = intersection
def difference(self, other):
inter = self&other
if not inter:
yield self
return
xs = {self.x1, self.x2}
ys = {self.y1, self.y2}
if self.x1<other.x1<self.x2: xs.add(other.x1)
if self.x1<other.x2<self.x2: xs.add(other.x2)
if self.y1<other.y1<self.y2: ys.add(other.y1)
if self.y1<other.y2<self.y2: ys.add(other.y2)
for (x1, x2), (y1, y2) in itertools.product(
pairwise(sorted(xs)), pairwise(sorted(ys))
):
rect = type(self)(x1, y1, x2, y2)
if rect!=inter:
yield rect
__sub__ = difference
def __init__(self, x1, y1, x2, y2):
if x1>x2 or y1>y2:
raise ValueError("Coordinates are invalid")
self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
def __iter__(self):
yield self.x1
yield self.y1
yield self.x2
yield self.y2
def __eq__(self, other):
return isinstance(other, Rectangle) and tuple(self)==tuple(other)
def __ne__(self, other):
return not (self==other)
def __repr__(self):
return type(self).__name__+repr(tuple(self))
def pairwise(iterable):
# https://docs.python.org/dev/library/itertools.html#recipes
a, b = itertools.tee(iterable)
next(b, None)
return zip(a, b)
# 1.
a = Rectangle(0, 0, 1, 1)
b = Rectangle(0.5, 0.5, 1.5, 1.5)
print(a&b)
# Rectangle(0.5, 0.5, 1, 1)
print(list(a-b))
# [Rectangle(0, 0, 0.5, 0.5), Rectangle(0, 0.5, 0.5, 1), Rectangle(0.5, 0, 1, 0.5)]
# 2.
b = Rectangle(0.25, 0.25, 1.25, 0.75)
print(a&b)
# Rectangle(0.25, 0.25, 1, 0.75)
print(list(a-b))
# [Rectangle(0, 0, 0.25, 0.25), Rectangle(0, 0.25, 0.25, 0.75), Rectangle(0, 0.75, 0.25, 1), Rectangle(0.25, 0, 1, 0.25), Rectangle(0.25, 0.75, 1, 1)]
# 3.
b = Rectangle(0.25, 0.25, 0.75, 0.75)
print(a&b)
# Rectangle(0.25, 0.25, 0.75, 0.75)
print(list(a-b))
# [Rectangle(0, 0, 0.25, 0.25), Rectangle(0, 0.25, 0.25, 0.75), Rectangle(0, 0.75, 0.25, 1), Rectangle(0.25, 0, 0.75, 0.25), Rectangle(0.25, 0.75, 0.75, 1), Rectangle(0.75, 0, 1, 0.25), Rectangle(0.75, 0.25, 1, 0.75), Rectangle(0.75, 0.75, 1, 1)]
# 4.
b = Rectangle(5, 5, 10, 10)
print(a&b)
# None
print(list(a-b))
# [Rectangle(0, 0, 1, 1)]
# 5.
b = Rectangle(-5, -5, 10, 10)
print(a&b)
# Rectangle(0, 0, 1, 1)
print(list(a-b))
# []
Intersection is based on SFML's implementation. It is proven correct and is not interesting to explain.
The difference, however, was a lot of fun to make.
Consider the following cases and compare them with corresponding examples at the bottom of the code. The method may return from 0 to 8 rectangles!
It works by finding all the vertical (xs
) and horizontal (ys
) lines that go through our rectangle (all the black and grey lines on the picture).
The coordinate sets are turned into sorted
lists and taken pairwise
([a, b, c]
becomes [(a, b), (b, c)]
).
The product
of such horizontal and vertical segments gives us all the rectangles that we divided the original one into by these lines.
All that remains is to yield
all of these rectangles except the intersection.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With