471 lines
17 KiB
Python
471 lines
17 KiB
Python
from __future__ import annotations
|
|
from enum import Enum
|
|
from math import gcd, sqrt, inf, atan2, degrees
|
|
from .math import round_half_up
|
|
from typing import Union, List
|
|
|
|
|
|
class DistanceAlgorithm(Enum):
|
|
MANHATTAN = 0
|
|
EUCLIDEAN = 1
|
|
PYTHAGOREAN = 1
|
|
CHEBYSHEV = 2
|
|
CHESSBOARD = 2
|
|
|
|
|
|
class Coordinate(tuple):
|
|
def __new__(cls, x: int | float, y: int | float, z: int | float | None = None):
|
|
return tuple.__new__(cls, (x, y, z))
|
|
|
|
@property
|
|
def x(self) -> int | float:
|
|
return self[0]
|
|
|
|
@property
|
|
def y(self) -> int | float:
|
|
return self[1]
|
|
|
|
@property
|
|
def z(self) -> int | float:
|
|
return self[2]
|
|
|
|
def is3D(self) -> bool:
|
|
return self[2] is not None
|
|
|
|
def getDistanceTo(
|
|
self,
|
|
target: Coordinate | tuple,
|
|
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
|
|
includeDiagonals: bool = False,
|
|
) -> int | float:
|
|
"""
|
|
Get distance to target Coordinate
|
|
|
|
:param target:
|
|
:param algorithm: Calculation Algorithm (s. DistanceAlgorithm)
|
|
:param includeDiagonals: in Manhattan Mode specify if diagonal
|
|
movements are allowed (counts as 1.4 in 2D, 1.7 in 3D)
|
|
:return: Distance to Target
|
|
"""
|
|
if algorithm == DistanceAlgorithm.EUCLIDEAN:
|
|
if self[2] is None:
|
|
return sqrt(
|
|
abs(self[0] - target[0]) ** 2 + abs(self[1] - target[1]) ** 2
|
|
)
|
|
else:
|
|
return sqrt(
|
|
abs(self[0] - target[0]) ** 2
|
|
+ abs(self[1] - target[1]) ** 2
|
|
+ abs(self[2] - target[2]) ** 2
|
|
)
|
|
elif algorithm == DistanceAlgorithm.CHEBYSHEV:
|
|
if self[2] is None:
|
|
return max(abs(target[0] - self[0]), abs(target[1] - self[1]))
|
|
else:
|
|
return max(
|
|
abs(target[0] - self[0]),
|
|
abs(target[1] - self[1]),
|
|
abs(target[2] - self[2]),
|
|
)
|
|
elif algorithm == DistanceAlgorithm.MANHATTAN:
|
|
if not includeDiagonals:
|
|
if self[2] is None:
|
|
return abs(self[0] - target[0]) + abs(self[1] - target[1])
|
|
else:
|
|
return (
|
|
abs(self[0] - target[0])
|
|
+ abs(self[1] - target[1])
|
|
+ abs(self[2] - target[2])
|
|
)
|
|
else:
|
|
dist = [abs(self[0] - target[0]), abs(self[1] - target[1])]
|
|
if self[2] is None:
|
|
o_dist = max(dist) - min(dist)
|
|
return o_dist + 1.4 * min(dist)
|
|
else:
|
|
dist.append(abs(self[2] - target[2]))
|
|
d_steps = min(dist)
|
|
dist.remove(min(dist))
|
|
dist = [x - d_steps for x in dist]
|
|
o_dist = max(dist) - min(dist)
|
|
return 1.7 * d_steps + o_dist + 1.4 * min(dist)
|
|
|
|
def inBoundaries(
|
|
self,
|
|
minX: int | float,
|
|
minY: int | float,
|
|
maxX: int | float,
|
|
maxY: int | float,
|
|
minZ: int | float = -inf,
|
|
maxZ: int | float = inf,
|
|
) -> bool:
|
|
if self[2] is None:
|
|
return minX <= self[0] <= maxX and minY <= self[1] <= maxY
|
|
else:
|
|
return (
|
|
minX <= self[0] <= maxX
|
|
and minY <= self[1] <= maxY
|
|
and minZ <= self[2] <= maxZ
|
|
)
|
|
|
|
def getCircle(
|
|
self,
|
|
radius: int | float = 1,
|
|
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
|
|
minX: int | float = -inf,
|
|
minY: int | float = -inf,
|
|
maxX: int | float = inf,
|
|
maxY: int | float = inf,
|
|
minZ: int | float = -inf,
|
|
maxZ: int | float = inf,
|
|
) -> list[Coordinate]:
|
|
ret = []
|
|
if self[2] is None: # mode 2D
|
|
for x in range(self[0] - radius * 2, self[0] + radius * 2 + 1):
|
|
for y in range(self[1] - radius * 2, self[1] + radius * 2 + 1):
|
|
target = Coordinate(x, y)
|
|
if not target.inBoundaries(minX, minY, maxX, maxY):
|
|
continue
|
|
dist = round_half_up(
|
|
self.getDistanceTo(
|
|
target, algorithm=algorithm, includeDiagonals=False
|
|
)
|
|
)
|
|
if dist == radius:
|
|
ret.append(target)
|
|
|
|
else:
|
|
for x in range(self[0] - radius * 2, self[0] + radius * 2 + 1):
|
|
for y in range(self[1] - radius * 2, self[1] + radius * 2 + 1):
|
|
for z in range(self[2] - radius * 2, self[2] + radius * 2 + 1):
|
|
target = Coordinate(x, y)
|
|
if not target.inBoundaries(minX, minY, maxX, maxY, minZ, maxZ):
|
|
continue
|
|
dist = round_half_up(
|
|
self.getDistanceTo(
|
|
target, algorithm=algorithm, includeDiagonals=False
|
|
)
|
|
)
|
|
if dist == radius:
|
|
ret.append(target)
|
|
|
|
return ret
|
|
|
|
def getNeighbours(
|
|
self,
|
|
includeDiagonal: bool = True,
|
|
minX: int | float = -inf,
|
|
minY: int | float = -inf,
|
|
maxX: int | float = inf,
|
|
maxY: int | float = inf,
|
|
minZ: int | float = -inf,
|
|
maxZ: int | float = inf,
|
|
dist: int | float = 1,
|
|
) -> list[Coordinate]:
|
|
"""
|
|
Get a list of neighbouring coordinates.
|
|
|
|
:param includeDiagonal: include diagonal neighbours
|
|
:param minX: ignore all neighbours that would have an X value below this
|
|
:param minY: ignore all neighbours that would have an Y value below this
|
|
:param minZ: ignore all neighbours that would have an Z value below this
|
|
:param maxX: ignore all neighbours that would have an X value above this
|
|
:param maxY: ignore all neighbours that would have an Y value above this
|
|
:param maxZ: ignore all neighbours that would have an Z value above this
|
|
:param dist: distance to neighbour coordinates
|
|
:return: list of Coordinate
|
|
"""
|
|
if self[2] is None:
|
|
if includeDiagonal:
|
|
nb_list = [
|
|
(-dist, -dist),
|
|
(-dist, 0),
|
|
(-dist, dist),
|
|
(0, -dist),
|
|
(0, dist),
|
|
(dist, -dist),
|
|
(dist, 0),
|
|
(dist, dist),
|
|
]
|
|
else:
|
|
nb_list = [(-dist, 0), (dist, 0), (0, -dist), (0, dist)]
|
|
|
|
for dx, dy in nb_list:
|
|
if minX <= self[0] + dx <= maxX and minY <= self[1] + dy <= maxY:
|
|
yield self.__class__(self[0] + dx, self[1] + dy)
|
|
else:
|
|
if includeDiagonal:
|
|
nb_list = [
|
|
(x, y, z)
|
|
for x in [-dist, 0, dist]
|
|
for y in [-dist, 0, dist]
|
|
for z in [-dist, 0, dist]
|
|
]
|
|
nb_list.remove((0, 0, 0))
|
|
else:
|
|
nb_list = [
|
|
(-dist, 0, 0),
|
|
(0, -dist, 0),
|
|
(dist, 0, 0),
|
|
(0, dist, 0),
|
|
(0, 0, dist),
|
|
(0, 0, -dist),
|
|
]
|
|
|
|
for dx, dy, dz in nb_list:
|
|
if (
|
|
minX <= self[0] + dx <= maxX
|
|
and minY <= self[1] + dy <= maxY
|
|
and minZ <= self[2] + dz <= maxZ
|
|
):
|
|
yield self.__class__(self[0] + dx, self[1] + dy, self[2] + dz)
|
|
|
|
def getAngleTo(self, target: Coordinate | tuple, normalized: bool = False) -> float:
|
|
"""normalized returns an angle going clockwise with 0 starting in the 'north'"""
|
|
if self[2] is not None:
|
|
raise NotImplementedError() # which angle?!?!
|
|
|
|
dx = target[0] - self[0]
|
|
dy = target[1] - self[1]
|
|
if not normalized:
|
|
return degrees(atan2(dy, dx))
|
|
else:
|
|
angle = degrees(atan2(dx, dy))
|
|
if dx >= 0:
|
|
return 180.0 - angle
|
|
else:
|
|
return 180.0 + abs(angle)
|
|
|
|
def getLineTo(self, target: Coordinate | tuple) -> List[Coordinate]:
|
|
"""this will probably not yield what you expect, when using float coordinates"""
|
|
if target == self:
|
|
return [self]
|
|
diff = target - self
|
|
|
|
if self[2] is None:
|
|
steps = gcd(diff[0], diff[1])
|
|
step_x = diff[0] // steps
|
|
step_y = diff[1] // steps
|
|
return [
|
|
self.__class__(self[0] + step_x * i, self[1] + step_y * i)
|
|
for i in range(steps + 1)
|
|
]
|
|
else:
|
|
steps = gcd(diff[0], diff[1], diff[2])
|
|
step_x = diff[0] // steps
|
|
step_y = diff[1] // steps
|
|
step_z = diff[2] // steps
|
|
return [
|
|
self.__class__(
|
|
self[0] + step_x * i, self[1] + step_y * i, self[2] + step_z * i
|
|
)
|
|
for i in range(steps + 1)
|
|
]
|
|
|
|
def reverse(self) -> Coordinate:
|
|
if self[2] is None:
|
|
return self.__class__(-self[0], -self[1])
|
|
else:
|
|
return self.__class__(-self[0], -self[1], -self[2])
|
|
|
|
def __hash__(self) -> int:
|
|
return hash((self[0], self[1], self[2]))
|
|
|
|
def __eq__(self, other: Coordinate | tuple) -> bool:
|
|
if self[2] is None:
|
|
return self[0] == other[0] and self[1] == other[1]
|
|
else:
|
|
return self[0] == other[0] and self[1] == other[1] and self[2] == other[2]
|
|
|
|
def __add__(self, other: Coordinate | tuple) -> Coordinate:
|
|
if self[2] is None:
|
|
return self.__class__(self[0] + other[0], self[1] + other[1])
|
|
else:
|
|
return self.__class__(
|
|
self[0] + other[0], self[1] + other[1], self[2] + other[2]
|
|
)
|
|
|
|
def __sub__(self, other: Coordinate | tuple) -> Coordinate:
|
|
if self[2] is None:
|
|
return self.__class__(self[0] - other[0], self[1] - other[1])
|
|
else:
|
|
return self.__class__(
|
|
self[0] - other[0], self[1] - other[1], self[2] - other[2]
|
|
)
|
|
|
|
def __mul__(self, other: int | float) -> Coordinate:
|
|
if self[2] is None:
|
|
return self.__class__(self[0] * other, self[1] * other)
|
|
else:
|
|
return self.__class__(self[0] * other, self[1] * other, self[2] * other)
|
|
|
|
def __mod__(self, other: int | float) -> Coordinate:
|
|
if self[2] is None:
|
|
return self.__class__(self[0] % other, self[1] % other)
|
|
else:
|
|
return self.__class__(self[0] % other, self[1] % other, self[2] % other)
|
|
|
|
def __floordiv__(self, other: int | float) -> Coordinate:
|
|
if self[2] is None:
|
|
return self.__class__(self[0] // other, self[1] // other)
|
|
else:
|
|
return self.__class__(self[0] // other, self[1] // other, self[2] // other)
|
|
|
|
def __truediv__(self, other: int | float) -> Coordinate:
|
|
if self[2] is None:
|
|
return self.__class__(self[0] / other, self[1] / other)
|
|
else:
|
|
return self.__class__(self[0] / other, self[1] / other, self[2] / other)
|
|
|
|
def __str__(self):
|
|
if self[2] is None:
|
|
return "({},{})".format(self[0], self[1])
|
|
else:
|
|
return "({},{},{})".format(self[0], self[1], self[2])
|
|
|
|
def __repr__(self):
|
|
if self[2] is None:
|
|
return "{}(x={}, y={})".format(self.__class__.__name__, self[0], self[1])
|
|
else:
|
|
return "{}(x={}, y={}, z={})".format(
|
|
self.__class__.__name__,
|
|
self[0],
|
|
self[1],
|
|
self[2],
|
|
)
|
|
|
|
@classmethod
|
|
def generate(
|
|
cls,
|
|
from_x: int | float,
|
|
to_x: int | float,
|
|
from_y: int | float,
|
|
to_y: int | float,
|
|
from_z: int | float = None,
|
|
to_z: int | float = None,
|
|
step: int | float = 1,
|
|
) -> List[Coordinate]:
|
|
if from_z is None or to_z is None:
|
|
return [
|
|
cls(x, y)
|
|
for x in range(from_x, to_x + step, step)
|
|
for y in range(from_y, to_y + step, step)
|
|
]
|
|
else:
|
|
return [
|
|
cls(x, y, z)
|
|
for x in range(from_x, to_x + step, step)
|
|
for y in range(from_y, to_y + step, step)
|
|
for z in range(from_z, to_z + step, step)
|
|
]
|
|
|
|
|
|
class Shape:
|
|
def __init__(self, top_left: Coordinate, bottom_right: Coordinate):
|
|
"""
|
|
in 2D mode: top_left is the upper left corner and bottom_right the lower right
|
|
(top_left.x <= bottom_right.x and top_left.y <= bottom_right.y)
|
|
in 3D mode: same logic applied, just for 3D Coordinates
|
|
top_left is the upper left rear corner and bottom_right the lower right front
|
|
(top_left.x <= bottom_right.x and top_left.y <= bottom_right.y and top_left.z <= bottom_right.z)
|
|
"""
|
|
self.top_left = top_left
|
|
self.bottom_right = bottom_right
|
|
self.mode_3d = top_left.z is not None and bottom_right.z is not None
|
|
|
|
def __len__(self):
|
|
if not self.mode_3d:
|
|
return (self.bottom_right.x - self.top_left.x + 1) * (
|
|
self.bottom_right.y - self.top_left.y + 1
|
|
)
|
|
else:
|
|
return (
|
|
(self.bottom_right.x - self.top_left.x + 1)
|
|
* (self.bottom_right.y - self.top_left.y + 1)
|
|
* (self.bottom_right.z - self.top_left.z + 1)
|
|
)
|
|
|
|
def intersection(self, other: Shape) -> Union[Shape, None]:
|
|
"""
|
|
returns a Shape of the intersecting part, or None if the Shapes don't intersect
|
|
"""
|
|
if self.mode_3d != other.mode_3d:
|
|
raise ValueError("Cannot calculate intersection between 2d and 3d shape")
|
|
|
|
if not self.mode_3d:
|
|
intersect_top_left = Coordinate(
|
|
self.top_left.x
|
|
if self.top_left.x > other.top_left.x
|
|
else other.top_left.x,
|
|
self.top_left.y
|
|
if self.top_left.y > other.top_left.y
|
|
else other.top_left.y,
|
|
)
|
|
intersect_bottom_right = Coordinate(
|
|
self.bottom_right.x
|
|
if self.bottom_right.x < other.bottom_right.x
|
|
else other.bottom_right.x,
|
|
self.bottom_right.y
|
|
if self.bottom_right.y < other.bottom_right.y
|
|
else other.bottom_right.y,
|
|
)
|
|
else:
|
|
intersect_top_left = Coordinate(
|
|
self.top_left.x
|
|
if self.top_left.x > other.top_left.x
|
|
else other.top_left.x,
|
|
self.top_left.y
|
|
if self.top_left.y > other.top_left.y
|
|
else other.top_left.y,
|
|
self.top_left.z
|
|
if self.top_left.z > other.top_left.z
|
|
else other.top_left.z,
|
|
)
|
|
intersect_bottom_right = Coordinate(
|
|
self.bottom_right.x
|
|
if self.bottom_right.x < other.bottom_right.x
|
|
else other.bottom_right.x,
|
|
self.bottom_right.y
|
|
if self.bottom_right.y < other.bottom_right.y
|
|
else other.bottom_right.y,
|
|
self.bottom_right.z
|
|
if self.bottom_right.z < other.bottom_right.z
|
|
else other.bottom_right.z,
|
|
)
|
|
|
|
if intersect_top_left <= intersect_bottom_right:
|
|
return self.__class__(intersect_top_left, intersect_bottom_right)
|
|
|
|
def __and__(self, other):
|
|
return self.intersection(other)
|
|
|
|
def __rand__(self, other):
|
|
return self.intersection(other)
|
|
|
|
def __str__(self):
|
|
return "%s(%s -> %s)" % (
|
|
self.__class__.__name__,
|
|
self.top_left,
|
|
self.bottom_right,
|
|
)
|
|
|
|
def __repr__(self):
|
|
return "%s(%s, %s)" % (
|
|
self.__class__.__name__,
|
|
self.top_left,
|
|
self.bottom_right,
|
|
)
|
|
|
|
|
|
class Square(Shape):
|
|
def __init__(self, top_left, bottom_right):
|
|
super(Square, self).__init__(top_left, bottom_right)
|
|
self.mode_3d = False
|
|
|
|
|
|
class Cube(Shape):
|
|
def __init__(self, top_left, bottom_right):
|
|
if top_left.z is None or bottom_right.z is None:
|
|
raise ValueError("Both Coordinates need to be 3D")
|
|
super(Cube, self).__init__(top_left, bottom_right)
|