Intersection and difference of two rectangles

The problem of intersection of two rectangles is very easy to solve. But difference of two rectangles is something considered much less frequently.

To clarify, we will be looking only at rectangles that are parallel to the 2D axes.

Rectangle intersection cases

Obviously, when one rectangle chomps away a part of another, typically it can no longer be represented as just one rectangle. The illustration shows these different cases. The solution described here represents the difference as a sum of multiple rectangles. It does not try to represent it in as few rectangles as possible, because there are multiple ways to do that and no objective way to pick one.


So if we have a Rectangle class that consists of 4 coordinates: x1, y1 (topleft corner), x2, y2 (bottomright corner), the intersection and difference functions would work like this (test cases are put in accordance to the illustration):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 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 simple. We look at the left edges of each rectangle and take whichever of them is more to the right — this will be the left edge of the new rectangle. Then the opposite happens for the right edges, and similar actions for top and bottom edges.

But there is also a possibility that the rectangles don't intersect. In case 4 the rightmost of the left edges is further to the right than the leftmost of the right edges. That (and the similar vertical case) will mean failure. So in the end we just need to check if the resulting rectangle has positive dimensions, and return it only in that case.


The difference, however, was a lot of fun to make.

As we can see from the illustration, the difference may consist of 0 to 8 rectangles!

The code below works by finding all the vertical (xs) and horizontal (ys) lines that go through our rectangle (all the white lines on the picture, which is the rectangle itself, and grey lines, which are the edges of the subtracted rectangle.

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.


Now, for the code!

Methods in the class are ordered illogically so that the important parts are visible without scrolling.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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):
        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):
    # //docs.python.org/dev/library/itertools.html#recipes
    a, b = itertools.tee(iterable)
    next(b, None)
    return zip(a, b)

(This was originally submitted on StackOverflow)

Created (last updated )
Comments powered by Disqus