coordinate.Shape: add __contains__()

coordinate.Line: add __contains__(), __len__() and proper __eq__()
NEW: coordinate.Polygon: deal with Polygons defined by a list of Coordinates (in (counter)?clockwise order. Currently allows for rectilinear decomposition and area calculation
This commit is contained in:
Stefan Harmuth 2024-01-01 21:48:17 +01:00
parent 94cffbbd74
commit 87fbaeafbe

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from enum import Enum from enum import Enum
from math import gcd, sqrt, inf, atan2, degrees, isclose 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, Iterable
from .tools import minmax from .tools import minmax
@ -379,6 +379,16 @@ class Shape:
def __rand__(self, other): def __rand__(self, other):
return self.intersection(other) return self.intersection(other)
def __contains__(self, item: Coordinate) -> bool:
if not self.mode_3d:
return self.top_left.x <= item.x <= self.bottom_right.x and self.top_left.y <= item.y <= self.bottom_right.y
else:
return (
self.top_left.x <= item.x <= self.bottom_right.x
and self.top_left.y <= item.y <= self.bottom_right.y
and self.top_left.z <= item.z <= self.bottom_right.z
)
def __str__(self): def __str__(self):
return "%s(%s -> %s)" % ( return "%s(%s -> %s)" % (
self.__class__.__name__, self.__class__.__name__,
@ -394,9 +404,9 @@ class Shape:
) )
class Square(Shape): class Rectangle(Shape):
def __init__(self, top_left, bottom_right): def __init__(self, top_left, bottom_right):
super(Square, self).__init__(top_left, bottom_right) super(Rectangle, self).__init__(top_left, bottom_right)
self.mode_3d = False self.mode_3d = False
@ -407,6 +417,7 @@ class Cube(Shape):
super(Cube, self).__init__(top_left, bottom_right) super(Cube, self).__init__(top_left, bottom_right)
# FIXME: Line could probably also just be a subclass of Shape
class Line: class Line:
def __init__(self, start: Coordinate, end: Coordinate): def __init__(self, start: Coordinate, end: Coordinate):
if start[2] is not None or end[2] is not None: if start[2] is not None or end[2] is not None:
@ -422,12 +433,6 @@ class Line:
def connects_to(self, other: Line) -> bool: def connects_to(self, other: Line) -> bool:
return self.start == other.start or self.start == other.end or self.end == other.start or self.end == other.end return self.start == other.start or self.start == other.end or self.end == other.start or self.end == other.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: def intersects(self, other: Line, strict: bool = True) -> bool:
try: try:
self.get_intersection(other, strict=strict) self.get_intersection(other, strict=strict)
@ -454,7 +459,7 @@ class Line:
if not strict: if not strict:
return ret return ret
else: else:
if self.contains(ret) and other.contains(ret): if ret in self and ret in other:
return ret return ret
else: else:
raise ValueError("intersection out of bounds") raise ValueError("intersection out of bounds")
@ -462,11 +467,79 @@ class Line:
def __hash__(self): def __hash__(self):
return hash((self.start, self.end)) return hash((self.start, self.end))
def __eq__(self, other: Line) -> bool:
return hash(self) == hash(other)
def __lt__(self, other: Line) -> bool: def __lt__(self, other: Line) -> bool:
return self.start < other.start return self.start < other.start
def __contains__(self, point: Coordinate | tuple) -> bool:
return isclose(
self.start.getDistanceTo(self.end),
self.start.getDistanceTo(point) + self.end.getDistanceTo(point),
)
def __len__(self) -> int:
return int(self.start.getDistanceTo(self.end))
def __str__(self): def __str__(self):
return f"Line({self.start} -> {self.end})" return f"Line({self.start} -> {self.end})"
def __repr__(self): def __repr__(self):
return str(self) return str(self)
class Polygon:
def __init__(self, points: list[Coordinate]) -> None:
"""points have to be in (counter)clockwise order, not repeating the first coordinate"""
if len(set(points)) != len(points):
raise ValueError("Polygon contains repeated points")
self.points = points
self.lines = set()
for i in range(len(points) - 1):
self.lines.add(Line(points[i], points[i + 1]))
self.lines.add(Line(points[-1], points[0]))
def get_circumference(self) -> float:
return sum(len(x) for x in self.lines)
def get_area(self) -> float:
S = 0
for i in range(len(self.points)):
S += (
self.points[i].x * self.points[(i + 1) % len(self.points)].y
- self.points[(i + 1) % len(self.points)].x * self.points[i].y
)
return abs(S) / 2
def decompose(self) -> Iterable[Rectangle]:
points_left = list(self.points)
def flip(point: Coordinate):
if point in points_left:
points_left.remove(point)
else:
points_left.append(point)
while points_left:
pk, pl, pm = None, None, None
for c in sorted(points_left, key=lambda p: (p[1], p[0])):
if pk is None:
pk = c
continue
if pl is None:
pl = c
continue
if pk.x <= c.x < pl.x and pk.y < c.y:
pm = c
break
flip(pk)
flip(pl)
flip(Coordinate(pk.x, pm.y))
flip(Coordinate(pl.x, pm.y))
yield Rectangle(pk, Coordinate(pl.x, pm.y))