"""
A module for interfacing with points.
"""
# built-in
from math import isclose, sqrt
from typing import NamedTuple, Union
# internal
from svgen.attribute import SimpleAttribute
from svgen.cartesian.mutate import Translation
[docs]
class PointAttrs(NamedTuple):
"""A grouping for 'x' and 'y' attributes."""
x: SimpleAttribute
y: SimpleAttribute
@property
def x_val(self) -> float:
"""Get the 'x' coordinate for these attributes."""
return float(self.x.value)
@property
def y_val(self) -> float:
"""Get the 'y' coordinate for these attributes."""
return float(self.y.value)
[docs]
class Point(NamedTuple):
"""A definition of a point in a Cartesian coordinate system."""
x: float = 0.0
y: float = 0.0
center: bool = False
idx: int = -1
[docs]
def translate(self, move: Union[Translation, float], *args) -> "Point":
"""Move a point by a given translation."""
move = (
Translation(move, *args)
if not isinstance(move, Translation)
else move
)
return Point(self.x + move.dx, self.y + move.dy, self.center, self.idx)
[docs]
def polar(
self,
count: float,
radius: float = 1.0,
degrees: bool = True,
ccw: bool = True,
) -> "Point":
"""Create a new point from a polar-coordinate translation."""
return self.translate(
Translation.polar(count, radius=radius, degrees=degrees, ccw=ccw)
)
[docs]
def with_index(self, idx: int) -> "Point":
"""Get a new point with the specified index, from an existing point."""
return Point(self.x, self.y, False, idx)
@property
def x_attr(self) -> SimpleAttribute:
"""Get the 'x' attribute for this point."""
attr = "x"
if self.center:
attr = "c" + attr
elif self.idx >= 0:
attr += str(self.idx)
return SimpleAttribute(attr, str(self.x))
@property
def y_attr(self) -> SimpleAttribute:
"""Get the 'y' attribute for this point."""
attr = "y"
if self.center:
attr = "c" + attr
elif self.idx >= 0:
attr += str(self.idx)
return SimpleAttribute(attr, str(self.y))
@property
def attrs(self) -> PointAttrs:
"""Get the 'x' and 'y' attributes for this point."""
return PointAttrs(self.x_attr, self.y_attr)
def __eq__(self, other: object) -> bool:
"""Determine if two points are the same."""
if not isinstance(other, Point):
return NotImplemented
# Use an absolute tolerance for comparisons with zero.
return isclose(self.x, other.x, abs_tol=1e-09) and isclose(
self.y, other.y, abs_tol=1e-09
)
[docs]
def distance(self, point: "Point") -> float:
"""Compute the distance from this point to another."""
run = point.x - self.x
rise = point.y - self.y
return sqrt(run * run + rise * rise)
[docs]
def to_center(self) -> "Point":
"""
Convert an existing point to one that is definitely a 'center' type.
"""
return Point(self.x, self.y, True)
[docs]
def to_center(point: Point) -> Point:
"""Convert an existing point to one that is definitely a 'center' type."""
return point.to_center()
DEFAULT = Point()
DEFAULT_CENTER = to_center(DEFAULT)
NamedPoints = dict[str, Point]
[docs]
class PointManager:
"""A class for managing points."""
def __init__(self, points: NamedPoints = None) -> None:
"""Initialize this point manager."""
if points is None:
points = {}
self.points = points
[docs]
def get_point(self, name: str) -> Point:
"""Access a point object."""
return self.points[name]
[docs]
def add_point(self, name: str, point: Union[Point, float], *args) -> Point:
"""Add a named point to this manager."""
if not isinstance(point, Point):
point = Point(point, *args)
assert name not in self.points, f"Already managing point '{name}'!"
self.points[name] = point
return self.points[name]
[docs]
def translate(self, move: Union[Translation, float], *args) -> None:
"""Translate all entities in the plane by some amount."""
for key, val in self.points.items():
self.points[key] = val.translate(move, *args)