# Simple geometry problems involving rectangles and circles in Python 3

I'm working with some simple geometry problems featuring rectangles and circles. My code is as follows:

# Working with simple geometry problems featuring rectangles and circles

import math

class Point():
"""Represents a point in 2-D space."""
def __init__(self, x=0, y=0):
self.x = x
self.y = y

def __str__(self):
return "(%.1f, %.1f)" % (self.x, self.y)

if isinstance(other, Point):
else:
return self.increment(other)

"""
Adds the x- and y-coordinates of two Points and returns a new Point with coordinates as the respective sums.

self, other: Point objects
"""
sumPoint = Point()
sumPoint.x = self.x + other.x
sumPoint.y = self.y + other.y
return sumPoint

def increment(self, other):
"""
Adds the respective coordinates of self and other -- which represent two Points -- and returns a new Point with coordinates as the respective sums.

self: Point object
other: tuple representing a point in 2-D space
"""
sumPoint = Point()
sumPoint.x = self.x + other[0]
sumPoint.y = self.y + other[1]
return sumPoint

class Rectangle():
"""
Represents a rectangle.
Attributes: width, height, corner (a Point object that specifies the lower-left corner)
"""
def __init__(self, width, height, corner):
self.width = width
self.height = height
self.corner = corner

def center(self):
"""
Returns the center of a Rectangle.
"""
p = Point()
p.x = self.corner.x + self.width/2
p.y = self.corner.y + self.height/2

return p

class Circle():
"""
Represents a circle in 2-D space.
Attributes: center (a Point object), radius
"""
self.center = center

def point_in_circle(self, point):
"""
Determines whether a point lies in or on the boundary of a Circle.

point: a Point object
returns: True if point lies in or on the boundary of the circle, else False
"""

center = Point(150, 100)
circle = Circle(center, 75)
# print(circle.center)

def distance_between_points(p1, p2):
"""Returns the distance between two points."""
return math.sqrt((p2.x - p1.x)**2 + (p2.y - p1.y)**2)

def rect_in_circle(rect, circle):
"""
Determines whether a rectangle lies entirely in or on the boundary of a circle.

rect: a Rectangle object
circle: a Circle object
returns: True if rect lies entirely in or on the boundary of circle, else False
"""
# declare and initialize the four vertices of rect
rectPoint1, rectPoint2, rectPoint3, rectPoint4 = Point(), Point(), Point(), Point()
rectPoint1.x, rectPoint1.y = rect.corner.x, rect.corner.y
rectPoint2.x, rectPoint2.y = rectPoint1.x + rect.width, rectPoint1.y
rectPoint3.x, rectPoint3.y = rectPoint1.x + rect.width, rectPoint1.y + rect.height
rectPoint4.x, rectPoint4.y = rectPoint1.x, rectPoint1.y + rect.height

# store the four vertices in a list
rectPoints = [rectPoint1, rectPoint2, rectPoint3, rectPoint4]

for pt in rectPoints:
# If any corner of the rectangle lies outside the boundary of the circle, then the rectangle cannot lie entirely in or on the boundary of the circle.
return False

return True

def rect_circle_overlap(rect, circle):
"""
Determines whether any part of the Rectangle falls inside the circle, i.e., whether the Rectangle and Circle intersect.

rect: a Rectangle object
circle: a Circle object
returns: True if any part of rect falls inside circle, else False
"""
if rect.width < rect.height:
if distance_between_points(rect.center(), circle.center) < (circle.radius + rect.width/2):
return True
else:
if distance_between_points(rect.center(), circle.center) < (circle.radius + rect.height/2):
return True

return False

box1 = Rectangle(100, 200, Point(50, 50))
box2 = Rectangle(50, 100, Point(0, 0))
box3 = Rectangle(100, 37.5, Point(100, 0))
box4 = Rectangle(50, 100, Point(125, 50))

# Tests

# For box1
assert(circle.point_in_circle(box1.corner)) == False
assert(rect_in_circle(box1, circle)) == False
assert(rect_circle_overlap(box1, circle)) == True

# For box2
assert(circle.point_in_circle(box2.corner)) == False
assert(rect_in_circle(box2, circle)) == False
assert(rect_circle_overlap(box2, circle)) == False

# For box3
assert(circle.point_in_circle(box3.corner)) == False
assert(rect_in_circle(box3, circle)) == False
assert(rect_circle_overlap(box3, circle)) == True

# For box4
assert(circle.point_in_circle(box4.corner)) == True
assert(rect_in_circle(box4, circle)) == True
assert(rect_circle_overlap(box4, circle)) == True

print("All tests passed.")


Can somebody check my logic for the point_in_circle Circle method and the rect_in_circle and rect_circle_overlap functions, please?

I wrote four tests:

1. Rectangle and circle that have a large overlap
2. Rectangle and circle that do NOT intersect at all
3. Rectangle and circle that have a small overlap
4. Rectangle fully inside a circle

How can I write more rigorous tests? In my program, I've only used assert statements. Can you test these functions yourself to check if they're working for all cases, please? And can this logic be used in rectangle-circle collision detection problems?

• Are you sure rect_circle_overlap does what you need? Suppose a rectangle centered at (0,0), h=2 and w=2016; and a circle centered at (1000,2) with radius 1.1 – they certainly overlap, although the distance between their centers, approx. 1000.002 is NOT less than the circle radius plus half the rect.height, i.e. 1.1+2/2 = 2.1. – CiaPan Sep 6 '16 at 15:20

By using a collections.namedtuple as the base class for your Point you gain the feature of accessing its elements both with p.x as well as p[0]. This makes your addition a lot easier. Also functions like __str__ and __repr__ are already defined, so we don't need those. I would, however, add the dist function here:

import math
import collections

class Point(collections.namedtuple("Coords", "x y")):
"""Represents a point in 2-D space."""
def __new__(cls, x=0, y=0):
return super(Point, cls).__new__(cls, x, y)

"""
Adds the respective coordinates of self and other -- which represent two Points -- and returns a new Point with coordinates as the respective sums.

self: Point object
other: tuple representing a point in 2-D space or a Point object
"""
return Point(self[0] + other[0], self[1] + other[1])

def dist(self, other):
"""Returns the distance between two points."""
return math.sqrt(sum((other[i] - self[i])**2 for i in len(self)))


Rectangle.center I would make a property and shorten it's code slightly:

class Rectangle:
def __init__(self, width, height, corner):
self.width = width
self.height = height
self.corner = corner
self.corners = (corner,
corner + (width, 0),
corner + (width, height),
corner + (0, height))

@property
def center(self):
"""
Returns the center of a Rectangle.
"""
x = self.corner.x + self.width/2
y = self.corner.y + self.height/2
return Point(x, y)


This allows you to do rect.center instead of rect.center().

I also added the other corners here to make the test for in a circle/intersection with circle easier.

Note: you also don't need empty parenthesis in a class declaration. They are only needed for inheritance.

Circle.point_in_circle can use Point.dist now. I would also make it the magic method __contains__. This way it can soak up the code of rect_in_circle as well (which can also be simplified a lot):

class Circle:
...
def __contains__(self, other):
"""
Determines whether a Point or a Rectangle lies in or on the boundary of a Circle.

other: a Point or Rectangle object
returns: True if other lies in or on the boundary of the circle, else False
"""
try:  # a Point
except AttributeError:
pass
# other is a Rectangle
return all(point in circle for point in other.corners)


The rect_in_circle part was made a lot easier, because we can use the defined addition between a Point and a tuple now. I also used the all function, which is short-circuited (and therefore identical to your for loop with a possible early return of False).

The intersection function can also be added as Circle.__and__ (which is the magic method to implement circle & rect which is the syntax for set intersections. It is easy to implement now:

class Circle:
...
def __and__(self, rect):
if self.center in rect or any(point in self for point in rect.corners):
return True
dist = Coords(abs(self.center.x - rect.center.x), abs(self.center.y - rect.center.y))
if dist.x > rect.width/2.0 + self.radius or dist.y > rect.height/2.0 + self.radius:
return False
if dist.x <= rect.width/2.0 or dist.y <= rect.height/2.0:
return True
corner_dist_sq = (dist.x - rect.width/2.0)**2 + (dist.y - rect.height/2.0)**2

__rand__ = __and__
intersect = __and__


I added __rand__ to allow also rect & circle.

I also added circle.intersect as a convenience, just like for sets.

For this to work I also added Rectangle.__contains__(self, point):

class Rectangle:
...
def __contains__(self, point):
return (self.corner[0] <= point[0] <= self.corner[0] + self.width and
self.corner[1] <= point[1] <= self.corner[1] + self.height)


I would also move all testing code into a if __name__ == "__main__": guard to allow importing parts of your code with import geometry from another script (if your file name is geometry.py).

Your tests can be written shorter (and more readable), with the above changes:

if __name__ == "__main__":
center = Point(150, 100)
circle = Circle(center, 75)
# print(circle.center)

box1 = Rectangle(100, 200, Point(50, 50))
box2 = Rectangle(50, 100, Point(0, 0))
box3 = Rectangle(100, 37.5, Point(100, 0))
box4 = Rectangle(50, 100, Point(125, 50))

# Tests

# For box1
assert box1.corner not in circle
assert box1 not in circle
assert circle & box1 == box1 & circle == circle.intersect(box1) == True

# For box2
assert box2.corner not in circle
assert box2 not in circle
assert box2 & circle == False

# For box3
assert box3.corner not in circle
assert box3 not in circle
assert box3 & circle

# For box4
assert box4.corner in circle
assert box4 in circle
assert box4 & circle

print("All tests passed.")


Note that assert is a key-word, just like in, for or if and therefore does not need parenthesis after it.

Finally, I would add a test-case where the circle and box intersect, but just on the side of the rect:

circle2 = Circle(Point(), 1)
box5 = Rectangle(2, 2, Point(1, -1))
assert circle2 & box5


In general I would choose better values for your test parameters. As they are now, it is really hard to visualize and know immediately what the result should be. At least make the numbers smaller and closer to the origin.

• Thanks a lot, Graipher. I really appreciate your review. I'm now aware of features that I didn't even know existed! – SassySamurai Sep 14 '16 at 5:48