3
\$\begingroup\$

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)

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

    def add_point(self, 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
    """
    def __init__(self, center, radius):
        self.center = center
        self.radius = radius

    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
        """
        return distance_between_points(point, self.center) <= self.radius

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.
        if distance_between_points(pt, circle.center) > circle.radius:
            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?

\$\endgroup\$
  • \$\begingroup\$ 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. \$\endgroup\$ – CiaPan Sep 6 '16 at 15:20
2
\$\begingroup\$

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)

    def __add__(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 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
            return other.dist(self.center) <= self.radius
        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
        return corner_dist_sq <= (circle.radius**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.

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.