The dunder add method only ever had one overload, so you don't need to declare an overload
		
			
				
	
	
		
			235 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			235 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# Eryn Wells <eryn@erynwells.me>
 | 
						|
 | 
						|
'''A bunch of geometric primitives'''
 | 
						|
 | 
						|
import math
 | 
						|
from dataclasses import dataclass
 | 
						|
from typing import Iterator, Optional, Tuple
 | 
						|
 | 
						|
 | 
						|
@dataclass(frozen=True)
 | 
						|
class Point:
 | 
						|
    '''A two-dimensional point, with coordinates in X and Y axes'''
 | 
						|
 | 
						|
    x: int = 0
 | 
						|
    y: int = 0
 | 
						|
 | 
						|
    @property
 | 
						|
    def neighbors(self) -> Iterator['Point']:
 | 
						|
        '''Iterator over the neighboring points of `self` in all eight directions.'''
 | 
						|
        for direction in Direction.all():
 | 
						|
            yield self + direction
 | 
						|
 | 
						|
    def is_adjacent_to(self, other: 'Point') -> bool:
 | 
						|
        '''Check if this point is adjacent to, but not overlapping the given point
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        other : Point
 | 
						|
            The point to check
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        bool
 | 
						|
            True if this point is adjacent to the other point
 | 
						|
        '''
 | 
						|
        return (self.x in (other.x - 1, other.x + 1)) and (self.y in (other.y - 1, other.y + 1))
 | 
						|
 | 
						|
    def direction_to_adjacent_point(self, other: 'Point') -> Optional['Vector']:
 | 
						|
        '''Given a point directly adjacent to `self`'''
 | 
						|
        for direction in Direction.all():
 | 
						|
            if (self + direction) != other:
 | 
						|
                continue
 | 
						|
            return direction
 | 
						|
 | 
						|
        return None
 | 
						|
 | 
						|
    def euclidean_distance_to(self, other: 'Point') -> float:
 | 
						|
        '''Compute the Euclidean distance to another Point'''
 | 
						|
        return math.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2)
 | 
						|
 | 
						|
    def __add__(self, other: 'Vector') -> 'Point':
 | 
						|
        if not isinstance(other, Vector):
 | 
						|
            raise TypeError('Only Vector can be added to a Point')
 | 
						|
        return Point(self.x + other.dx, self.y + other.dy)
 | 
						|
 | 
						|
    def __sub__(self, other: 'Vector') -> 'Point':
 | 
						|
        if not isinstance(other, Vector):
 | 
						|
            raise TypeError('Only Vector can be added to a Point')
 | 
						|
        return Point(self.x - other.dx, self.y - other.dy)
 | 
						|
 | 
						|
    def __iter__(self):
 | 
						|
        yield self.x
 | 
						|
        yield self.y
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f'(x:{self.x}, y:{self.y})'
 | 
						|
 | 
						|
 | 
						|
@dataclass(frozen=True)
 | 
						|
class Vector:
 | 
						|
    '''A two-dimensional vector, representing change in position in X and Y axes'''
 | 
						|
 | 
						|
    dx: int = 0
 | 
						|
    dy: int = 0
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def from_point(cls, point: Point) -> 'Vector':
 | 
						|
        '''Create a Vector from a Point'''
 | 
						|
        return Vector(point.x, point.y)
 | 
						|
 | 
						|
    def __iter__(self):
 | 
						|
        yield self.dx
 | 
						|
        yield self.dy
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f'(δx:{self.dx}, δy:{self.dy})'
 | 
						|
 | 
						|
 | 
						|
class Direction:
 | 
						|
    '''
 | 
						|
    A collection of simple uint vectors in each of the eight major compass
 | 
						|
    directions. This is a namespace, not a class.
 | 
						|
    '''
 | 
						|
 | 
						|
    North = Vector(0, -1)
 | 
						|
    NorthEast = Vector(1, -1)
 | 
						|
    East = Vector(1, 0)
 | 
						|
    SouthEast = Vector(1, 1)
 | 
						|
    South = Vector(0, 1)
 | 
						|
    SouthWest = Vector(-1, 1)
 | 
						|
    West = Vector(-1, 0)
 | 
						|
    NorthWest = Vector(-1, -1)
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def all(cls) -> Iterator[Vector]:
 | 
						|
        '''Iterate through all directions, starting with North and proceeding clockwise'''
 | 
						|
        yield Direction.North
 | 
						|
        yield Direction.NorthEast
 | 
						|
        yield Direction.East
 | 
						|
        yield Direction.SouthEast
 | 
						|
        yield Direction.South
 | 
						|
        yield Direction.SouthWest
 | 
						|
        yield Direction.West
 | 
						|
        yield Direction.NorthWest
 | 
						|
 | 
						|
 | 
						|
@dataclass(frozen=True)
 | 
						|
class Size:
 | 
						|
    '''A two-dimensional size, representing size in X (width) and Y (height) axes'''
 | 
						|
 | 
						|
    width: int = 0
 | 
						|
    height: int = 0
 | 
						|
 | 
						|
    @property
 | 
						|
    def numpy_shape(self) -> Tuple[int, int]:
 | 
						|
        '''Return a tuple suitable for passing into numpy array initializers for specifying the shape of the array.'''
 | 
						|
        return (self.width, self.height)
 | 
						|
 | 
						|
    def __iter__(self):
 | 
						|
        yield self.width
 | 
						|
        yield self.height
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f'(w:{self.width}, h:{self.height})'
 | 
						|
 | 
						|
 | 
						|
@dataclass(frozen=True)
 | 
						|
class Rect:
 | 
						|
    '''A two-dimensional rectangle, defined by an origin point and size'''
 | 
						|
 | 
						|
    origin: Point
 | 
						|
    size: Size
 | 
						|
 | 
						|
    @property
 | 
						|
    def min_x(self) -> int:
 | 
						|
        '''Minimum x-value that is still within the bounds of this rectangle. This is the origin's x-value.'''
 | 
						|
        return self.origin.x
 | 
						|
 | 
						|
    @property
 | 
						|
    def min_y(self) -> int:
 | 
						|
        '''Minimum y-value that is still within the bounds of this rectangle. This is the origin's y-value.'''
 | 
						|
        return self.origin.y
 | 
						|
 | 
						|
    @property
 | 
						|
    def mid_x(self) -> int:
 | 
						|
        '''The x-value of the center point of this rectangle.'''
 | 
						|
        return self.origin.x + self.size.width // 2
 | 
						|
 | 
						|
    @property
 | 
						|
    def mid_y(self) -> int:
 | 
						|
        '''The y-value of the center point of this rectangle.'''
 | 
						|
        return self.origin.y + self.size.height // 2
 | 
						|
 | 
						|
    @property
 | 
						|
    def max_x(self) -> int:
 | 
						|
        '''Maximum x-value that is still within the bounds of this rectangle.'''
 | 
						|
        return self.origin.x + self.size.width - 1
 | 
						|
 | 
						|
    @property
 | 
						|
    def max_y(self) -> int:
 | 
						|
        '''Maximum y-value that is still within the bounds of this rectangle.'''
 | 
						|
        return self.origin.y + self.size.height - 1
 | 
						|
 | 
						|
    @property
 | 
						|
    def width(self) -> int:
 | 
						|
        '''The width of the rectangle. A convenience property for accessing `self.size.width`.'''
 | 
						|
        return self.size.width
 | 
						|
 | 
						|
    @property
 | 
						|
    def height(self) -> int:
 | 
						|
        '''The height of the rectangle. A convenience property for accessing `self.size.height`.'''
 | 
						|
        return self.size.height
 | 
						|
 | 
						|
    @property
 | 
						|
    def midpoint(self) -> Point:
 | 
						|
        '''A Point in the middle of the Rect'''
 | 
						|
        return Point(self.mid_x, self.mid_y)
 | 
						|
 | 
						|
    def intersects(self, other: 'Rect') -> bool:
 | 
						|
        '''Returns `True` if `other` intersects this Rect.'''
 | 
						|
        if other.min_x > self.max_x:
 | 
						|
            return False
 | 
						|
 | 
						|
        if other.max_x < self.min_x:
 | 
						|
            return False
 | 
						|
 | 
						|
        if other.min_y > self.max_y:
 | 
						|
            return False
 | 
						|
 | 
						|
        if other.max_y < self.min_y:
 | 
						|
            return False
 | 
						|
 | 
						|
        return True
 | 
						|
 | 
						|
    def inset_rect(self, top: int = 0, right: int = 0, bottom: int = 0, left: int = 0) -> 'Rect':
 | 
						|
        '''
 | 
						|
        Return a new Rect inset from this rect by the specified values. Arguments are listed in clockwise order around
 | 
						|
        the permeter. This method doesn't validate the returned Rect, or transform it to a canonical representation with
 | 
						|
        the origin at the top-left.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        top : int
 | 
						|
            Amount to inset from the top
 | 
						|
        right : int
 | 
						|
            Amount to inset from the right
 | 
						|
        bottom : int
 | 
						|
            Amount to inset from the bottom
 | 
						|
        left : int
 | 
						|
            Amount to inset from the left
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        Rect
 | 
						|
            A new Rect, inset from `self` by the given amount on each side
 | 
						|
        '''
 | 
						|
        return Rect(Point(self.origin.x + left, self.origin.y + top),
 | 
						|
                    Size(self.size.width - right - left, self.size.height - top - bottom))
 | 
						|
 | 
						|
    def __iter__(self):
 | 
						|
        yield tuple(self.origin)
 | 
						|
        yield tuple(self.size)
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f'[{self.origin}, {self.size}]'
 |