Compare commits
3 Commits
14d535911c
...
1e43a2ff0d
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e43a2ff0d | |||
| cc1fd86ede | |||
| 9386c40ea5 |
@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from math import gcd, sqrt, inf, atan2, degrees
|
from math import gcd, sqrt, inf, atan2, degrees, isclose
|
||||||
from .math import round_half_up
|
from .math import round_half_up
|
||||||
from typing import Union, List
|
from typing import Union, List
|
||||||
|
|
||||||
@ -14,19 +14,19 @@ class DistanceAlgorithm(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class Coordinate(tuple):
|
class Coordinate(tuple):
|
||||||
def __new__(cls, x: int, y: int, z: int = None) -> Coordinate:
|
def __new__(cls, x: int | float, y: int | float, z: int | float | None = None):
|
||||||
return tuple.__new__(Coordinate, (x, y, z))
|
return tuple.__new__(cls, (x, y, z))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def x(self):
|
def x(self) -> int | float:
|
||||||
return self[0]
|
return self[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def y(self):
|
def y(self) -> int | float:
|
||||||
return self[1]
|
return self[1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def z(self):
|
def z(self) -> int | float:
|
||||||
return self[2]
|
return self[2]
|
||||||
|
|
||||||
def is3D(self) -> bool:
|
def is3D(self) -> bool:
|
||||||
@ -37,7 +37,7 @@ class Coordinate(tuple):
|
|||||||
target: Coordinate | tuple,
|
target: Coordinate | tuple,
|
||||||
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
|
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
|
||||||
includeDiagonals: bool = False,
|
includeDiagonals: bool = False,
|
||||||
) -> Union[int, float]:
|
) -> int | float:
|
||||||
"""
|
"""
|
||||||
Get distance to target Coordinate
|
Get distance to target Coordinate
|
||||||
|
|
||||||
@ -49,7 +49,9 @@ class Coordinate(tuple):
|
|||||||
"""
|
"""
|
||||||
if algorithm == DistanceAlgorithm.EUCLIDEAN:
|
if algorithm == DistanceAlgorithm.EUCLIDEAN:
|
||||||
if self[2] is None:
|
if self[2] is None:
|
||||||
return sqrt(abs(self[0] - target[0]) ** 2 + abs(self[1] - target[1]) ** 2)
|
return sqrt(
|
||||||
|
abs(self[0] - target[0]) ** 2 + abs(self[1] - target[1]) ** 2
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return sqrt(
|
return sqrt(
|
||||||
abs(self[0] - target[0]) ** 2
|
abs(self[0] - target[0]) ** 2
|
||||||
@ -90,12 +92,12 @@ class Coordinate(tuple):
|
|||||||
|
|
||||||
def inBoundaries(
|
def inBoundaries(
|
||||||
self,
|
self,
|
||||||
minX: int,
|
minX: int | float,
|
||||||
minY: int,
|
minY: int | float,
|
||||||
maxX: int,
|
maxX: int | float,
|
||||||
maxY: int,
|
maxY: int | float,
|
||||||
minZ: int = -inf,
|
minZ: int | float = -inf,
|
||||||
maxZ: int = inf,
|
maxZ: int | float = inf,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if self[2] is None:
|
if self[2] is None:
|
||||||
return minX <= self[0] <= maxX and minY <= self[1] <= maxY
|
return minX <= self[0] <= maxX and minY <= self[1] <= maxY
|
||||||
@ -108,14 +110,14 @@ class Coordinate(tuple):
|
|||||||
|
|
||||||
def getCircle(
|
def getCircle(
|
||||||
self,
|
self,
|
||||||
radius: int = 1,
|
radius: int | float = 1,
|
||||||
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
|
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
|
||||||
minX: int = -inf,
|
minX: int | float = -inf,
|
||||||
minY: int = -inf,
|
minY: int | float = -inf,
|
||||||
maxX: int = inf,
|
maxX: int | float = inf,
|
||||||
maxY: int = inf,
|
maxY: int | float = inf,
|
||||||
minZ: int = -inf,
|
minZ: int | float = -inf,
|
||||||
maxZ: int = inf,
|
maxZ: int | float = inf,
|
||||||
) -> list[Coordinate]:
|
) -> list[Coordinate]:
|
||||||
ret = []
|
ret = []
|
||||||
if self[2] is None: # mode 2D
|
if self[2] is None: # mode 2D
|
||||||
@ -152,12 +154,13 @@ class Coordinate(tuple):
|
|||||||
def getNeighbours(
|
def getNeighbours(
|
||||||
self,
|
self,
|
||||||
includeDiagonal: bool = True,
|
includeDiagonal: bool = True,
|
||||||
minX: int = -inf,
|
minX: int | float = -inf,
|
||||||
minY: int = -inf,
|
minY: int | float = -inf,
|
||||||
maxX: int = inf,
|
maxX: int | float = inf,
|
||||||
maxY: int = inf,
|
maxY: int | float = inf,
|
||||||
minZ: int = -inf,
|
minZ: int | float = -inf,
|
||||||
maxZ: int = inf,
|
maxZ: int | float = inf,
|
||||||
|
dist: int | float = 1,
|
||||||
) -> list[Coordinate]:
|
) -> list[Coordinate]:
|
||||||
"""
|
"""
|
||||||
Get a list of neighbouring coordinates.
|
Get a list of neighbouring coordinates.
|
||||||
@ -169,22 +172,23 @@ class Coordinate(tuple):
|
|||||||
:param maxX: ignore all neighbours that would have an X value above 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 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 maxZ: ignore all neighbours that would have an Z value above this
|
||||||
|
:param dist: distance to neighbour coordinates
|
||||||
:return: list of Coordinate
|
:return: list of Coordinate
|
||||||
"""
|
"""
|
||||||
if self[2] is None:
|
if self[2] is None:
|
||||||
if includeDiagonal:
|
if includeDiagonal:
|
||||||
nb_list = [
|
nb_list = [
|
||||||
(-1, -1),
|
(-dist, -dist),
|
||||||
(-1, 0),
|
(-dist, 0),
|
||||||
(-1, 1),
|
(-dist, dist),
|
||||||
(0, -1),
|
(0, -dist),
|
||||||
(0, 1),
|
(0, dist),
|
||||||
(1, -1),
|
(dist, -dist),
|
||||||
(1, 0),
|
(dist, 0),
|
||||||
(1, 1),
|
(dist, dist),
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
nb_list = [(-1, 0), (1, 0), (0, -1), (0, 1)]
|
nb_list = [(-dist, 0), (dist, 0), (0, -dist), (0, dist)]
|
||||||
|
|
||||||
for dx, dy in nb_list:
|
for dx, dy in nb_list:
|
||||||
if minX <= self[0] + dx <= maxX and minY <= self[1] + dy <= maxY:
|
if minX <= self[0] + dx <= maxX and minY <= self[1] + dy <= maxY:
|
||||||
@ -193,19 +197,19 @@ class Coordinate(tuple):
|
|||||||
if includeDiagonal:
|
if includeDiagonal:
|
||||||
nb_list = [
|
nb_list = [
|
||||||
(x, y, z)
|
(x, y, z)
|
||||||
for x in [-1, 0, 1]
|
for x in [-dist, 0, dist]
|
||||||
for y in [-1, 0, 1]
|
for y in [-dist, 0, dist]
|
||||||
for z in [-1, 0, 1]
|
for z in [-dist, 0, dist]
|
||||||
]
|
]
|
||||||
nb_list.remove((0, 0, 0))
|
nb_list.remove((0, 0, 0))
|
||||||
else:
|
else:
|
||||||
nb_list = [
|
nb_list = [
|
||||||
(-1, 0, 0),
|
(-dist, 0, 0),
|
||||||
(0, -1, 0),
|
(0, -dist, 0),
|
||||||
(1, 0, 0),
|
(dist, 0, 0),
|
||||||
(0, 1, 0),
|
(0, dist, 0),
|
||||||
(0, 0, 1),
|
(0, 0, dist),
|
||||||
(0, 0, -1),
|
(0, 0, -dist),
|
||||||
]
|
]
|
||||||
|
|
||||||
for dx, dy, dz in nb_list:
|
for dx, dy, dz in nb_list:
|
||||||
@ -233,6 +237,7 @@ class Coordinate(tuple):
|
|||||||
return 180.0 + abs(angle)
|
return 180.0 + abs(angle)
|
||||||
|
|
||||||
def getLineTo(self, target: Coordinate | tuple) -> List[Coordinate]:
|
def getLineTo(self, target: Coordinate | tuple) -> List[Coordinate]:
|
||||||
|
"""this will probably not yield what you expect, when using float coordinates"""
|
||||||
if target == self:
|
if target == self:
|
||||||
return [self]
|
return [self]
|
||||||
diff = target - self
|
diff = target - self
|
||||||
@ -276,21 +281,25 @@ class Coordinate(tuple):
|
|||||||
if self[2] is None:
|
if self[2] is None:
|
||||||
return self.__class__(self[0] + other[0], self[1] + other[1])
|
return self.__class__(self[0] + other[0], self[1] + other[1])
|
||||||
else:
|
else:
|
||||||
return self.__class__(self[0] + other[0], self[1] + other[1], self[2] + other[2])
|
return self.__class__(
|
||||||
|
self[0] + other[0], self[1] + other[1], self[2] + other[2]
|
||||||
|
)
|
||||||
|
|
||||||
def __sub__(self, other: Coordinate | tuple) -> Coordinate:
|
def __sub__(self, other: Coordinate | tuple) -> Coordinate:
|
||||||
if self[2] is None:
|
if self[2] is None:
|
||||||
return self.__class__(self[0] - other[0], self[1] - other[1])
|
return self.__class__(self[0] - other[0], self[1] - other[1])
|
||||||
else:
|
else:
|
||||||
return self.__class__(self[0] - other[0], self[1] - other[1], self[2] - other[2])
|
return self.__class__(
|
||||||
|
self[0] - other[0], self[1] - other[1], self[2] - other[2]
|
||||||
|
)
|
||||||
|
|
||||||
def __mul__(self, other: int) -> Coordinate:
|
def __mul__(self, other: int | float) -> Coordinate:
|
||||||
if self[2] is None:
|
if self[2] is None:
|
||||||
return self.__class__(self[0] * other, self[1] * other)
|
return self.__class__(self[0] * other, self[1] * other)
|
||||||
else:
|
else:
|
||||||
return self.__class__(self[0] * other, self[1] * other, self[2] * other)
|
return self.__class__(self[0] * other, self[1] * other, self[2] * other)
|
||||||
|
|
||||||
def __mod__(self, other: int) -> Coordinate:
|
def __mod__(self, other: int | float) -> Coordinate:
|
||||||
if self[2] is None:
|
if self[2] is None:
|
||||||
return self.__class__(self[0] % other, self[1] % other)
|
return self.__class__(self[0] % other, self[1] % other)
|
||||||
else:
|
else:
|
||||||
@ -303,19 +312,22 @@ class Coordinate(tuple):
|
|||||||
return self.__class__(self[0] // other, self[1] // other, self[2] // other)
|
return self.__class__(self[0] // other, self[1] // other, self[2] // other)
|
||||||
|
|
||||||
def __truediv__(self, other: int | float) -> Coordinate:
|
def __truediv__(self, other: int | float) -> Coordinate:
|
||||||
return self // other
|
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):
|
def __str__(self):
|
||||||
if self[2] is None:
|
if self[2] is None:
|
||||||
return "(%d,%d)" % (self[0], self[1])
|
return "({},{})".format(self[0], self[1])
|
||||||
else:
|
else:
|
||||||
return "(%d,%d,%d)" % (self[0], self[1], self[2])
|
return "({},{},{})".format(self[0], self[1], self[2])
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self[2] is None:
|
if self[2] is None:
|
||||||
return "%s(x=%d, y=%d)" % (self.__class__.__name__, self[0], self[1])
|
return "{}(x={}, y={})".format(self.__class__.__name__, self[0], self[1])
|
||||||
else:
|
else:
|
||||||
return "%s(x=%d, y=%d, z=%d)" % (
|
return "{}(x={}, y={}, z={})".format(
|
||||||
self.__class__.__name__,
|
self.__class__.__name__,
|
||||||
self[0],
|
self[0],
|
||||||
self[1],
|
self[1],
|
||||||
@ -325,112 +337,29 @@ class Coordinate(tuple):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def generate(
|
def generate(
|
||||||
cls,
|
cls,
|
||||||
from_x: int,
|
from_x: int | float,
|
||||||
to_x: int,
|
to_x: int | float,
|
||||||
from_y: int,
|
from_y: int | float,
|
||||||
to_y: int,
|
to_y: int | float,
|
||||||
from_z: int = None,
|
from_z: int | float = None,
|
||||||
to_z: int = None,
|
to_z: int | float = None,
|
||||||
|
step: int | float = 1,
|
||||||
) -> List[Coordinate]:
|
) -> List[Coordinate]:
|
||||||
if from_z is None or to_z is None:
|
if from_z is None or to_z is None:
|
||||||
return [
|
return [
|
||||||
cls(x, y)
|
cls(x, y)
|
||||||
for x in range(from_x, to_x + 1)
|
for x in range(from_x, to_x + step, step)
|
||||||
for y in range(from_y, to_y + 1)
|
for y in range(from_y, to_y + step, step)
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
return [
|
return [
|
||||||
cls(x, y, z)
|
cls(x, y, z)
|
||||||
for x in range(from_x, to_x + 1)
|
for x in range(from_x, to_x + step, step)
|
||||||
for y in range(from_y, to_y + 1)
|
for y in range(from_y, to_y + step, step)
|
||||||
for z in range(from_z, to_z + 1)
|
for z in range(from_z, to_z + step, step)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class HexCoordinate(Coordinate):
|
|
||||||
"""
|
|
||||||
https://www.redblobgames.com/grids/hexagons/#coordinates-cube
|
|
||||||
Treat as 3d Coordinate
|
|
||||||
+y -x +z
|
|
||||||
y x z
|
|
||||||
yxz
|
|
||||||
z x y
|
|
||||||
-z +x -y
|
|
||||||
"""
|
|
||||||
|
|
||||||
neighbour_vectors = {
|
|
||||||
"ne": Coordinate(-1, 0, 1),
|
|
||||||
"nw": Coordinate(-1, 1, 0),
|
|
||||||
"e": Coordinate(0, -1, 1),
|
|
||||||
"w": Coordinate(0, 1, -1),
|
|
||||||
"sw": Coordinate(1, 0, -1),
|
|
||||||
"se": Coordinate(1, -1, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, x: int, y: int, z: int):
|
|
||||||
assert (x + y + z) == 0
|
|
||||||
super(HexCoordinate, self).__init__(x, y, z)
|
|
||||||
|
|
||||||
def get_length(self) -> int:
|
|
||||||
return (abs(self.x) + abs(self.y) + abs(self.z)) // 2
|
|
||||||
|
|
||||||
def getDistanceTo(
|
|
||||||
self,
|
|
||||||
target: Coordinate,
|
|
||||||
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
|
|
||||||
includeDiagonals: bool = True,
|
|
||||||
) -> Union[int, float]:
|
|
||||||
# includeDiagonals makes no sense in a hex grid, it's just here for signature reasons
|
|
||||||
if algorithm == DistanceAlgorithm.MANHATTAN:
|
|
||||||
return (self - target).get_length()
|
|
||||||
|
|
||||||
def getNeighbours(
|
|
||||||
self,
|
|
||||||
includeDiagonal: bool = True,
|
|
||||||
minX: int = -inf,
|
|
||||||
minY: int = -inf,
|
|
||||||
maxX: int = inf,
|
|
||||||
maxY: int = inf,
|
|
||||||
minZ: int = -inf,
|
|
||||||
maxZ: int = inf,
|
|
||||||
) -> list[Coordinate]:
|
|
||||||
# includeDiagonals makes no sense in a hex grid, it's just here for signature reasons
|
|
||||||
return [
|
|
||||||
self + x
|
|
||||||
for x in self.neighbour_vectors.values()
|
|
||||||
if minX <= (self + x).x <= maxX
|
|
||||||
and minY <= (self + x).y <= maxY
|
|
||||||
and minZ <= (self + x).z <= maxZ
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
HexCoordinateR = HexCoordinate
|
|
||||||
|
|
||||||
|
|
||||||
class HexCoordinateF(HexCoordinate):
|
|
||||||
"""
|
|
||||||
https://www.redblobgames.com/grids/hexagons/#coordinates-cube
|
|
||||||
Treat as 3d Coordinate
|
|
||||||
+y -x
|
|
||||||
y x
|
|
||||||
-z z yxz z +z
|
|
||||||
x y
|
|
||||||
+x -y
|
|
||||||
"""
|
|
||||||
|
|
||||||
neighbour_vectors = {
|
|
||||||
"ne": Coordinate(-1, 0, 1),
|
|
||||||
"nw": Coordinate(0, 1, -1),
|
|
||||||
"n": Coordinate(-1, 1, 0),
|
|
||||||
"s": Coordinate(1, -1, 0),
|
|
||||||
"sw": Coordinate(1, 0, -1),
|
|
||||||
"se": Coordinate(0, -1, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, x: int, y: int, z: int):
|
|
||||||
super(HexCoordinateF, self).__init__(x, y, z)
|
|
||||||
|
|
||||||
|
|
||||||
class Shape:
|
class Shape:
|
||||||
def __init__(self, top_left: Coordinate, bottom_right: Coordinate):
|
def __init__(self, top_left: Coordinate, bottom_right: Coordinate):
|
||||||
"""
|
"""
|
||||||
@ -539,3 +468,48 @@ class Cube(Shape):
|
|||||||
if top_left.z is None or bottom_right.z is None:
|
if top_left.z is None or bottom_right.z is None:
|
||||||
raise ValueError("Both Coordinates need to be 3D")
|
raise ValueError("Both Coordinates need to be 3D")
|
||||||
super(Cube, self).__init__(top_left, bottom_right)
|
super(Cube, self).__init__(top_left, bottom_right)
|
||||||
|
|
||||||
|
|
||||||
|
class Line:
|
||||||
|
def __init__(self, start: Coordinate, end: Coordinate):
|
||||||
|
if start[2] is not None or end[2] is not None:
|
||||||
|
raise NotImplementedError("3D Lines are hard(er)")
|
||||||
|
self.start = start
|
||||||
|
self.end = end
|
||||||
|
|
||||||
|
def contains(self, point: Coordinate | tuple) -> bool:
|
||||||
|
return isclose(
|
||||||
|
self.start.getDistanceTo(self.end),
|
||||||
|
self.start.getDistanceTo(point) + self.end.getDistanceTo(point),
|
||||||
|
)
|
||||||
|
|
||||||
|
def intersects(self, other: Line, strict: bool = True) -> bool:
|
||||||
|
try:
|
||||||
|
self.get_intersection(other, strict=strict)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_intersection(self, other: Line, strict: bool = True) -> Coordinate:
|
||||||
|
xdiff = (self.start[0] - self.end[0], other.start[0] - other.end[0])
|
||||||
|
ydiff = (self.start[1] - self.end[1], other.start[1] - other.end[1])
|
||||||
|
|
||||||
|
def det(a, b):
|
||||||
|
return a[0] * b[1] - a[1] * b[0]
|
||||||
|
|
||||||
|
div = det(xdiff, ydiff)
|
||||||
|
if div == 0:
|
||||||
|
raise ValueError("lines do not intersect")
|
||||||
|
|
||||||
|
d = (det(self.start, self.end), det(other.start, other.end))
|
||||||
|
x = det(d, xdiff) / div
|
||||||
|
y = det(d, ydiff) / div
|
||||||
|
ret = Coordinate(x, y)
|
||||||
|
|
||||||
|
if not strict:
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
if self.contains(ret) and other.contains(ret):
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
raise ValueError("intersection out of bounds")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user