import math from .. import util from ..euclid.shapes import Circle as ECircle from .. import poincare as module from . import shapes class Point: def __init__(self, x, y, hr=None, theta=None): self.x = x self.y = y # Hyperbolic polar coordinates if theta is None: theta = math.atan2(y, x) if hr is None: r = math.hypot(x, y) if self.isIdeal(): hr = float('inf') else: hr = 2 * math.atanh(r) self.theta = theta self.hr = hr def __iter__(self): return iter((self.x, self.y)) def __getitem__(self, i): return (self.x, self.y)[i] def __len__(self): return 2 def isIdeal(self): return util.nearZero(math.hypot(self.x, self.y) - 1) def polarAngleTo(self, p2, origin=None): if origin is None: return p2.theta - self.theta else: assert False, 'TODO' def distanceTo(self, p2): r1, t1 = self.hr, self.theta r2, t2 = p2.hr, p2.theta d = math.acosh(math.cosh(r1)*math.cosh(r2) - math.sinh(r1)*math.sinh(r2)*math.cos(t2-t1)) return d def midpointWith(self, p2, frac=0.5): d = self.distanceTo(p2) pMid = Point.fromHPolar(d*frac, 0) return module.Transform.translation(self, p2).applyToPoint(pMid) @staticmethod def fromEuclid(x, y): r = math.hypot(x, y) if util.nearZero(r - 1.0): return Ideal(math.atan2(y, x)) elif r < 1.0: return Point(x, y) else: raise ValueError('Euclidean coordinates are outside the unit circle') @staticmethod def fromPolarEuclid(r, rad=None, deg=None): assert (rad is None) != (deg is None) if rad is None: rad = math.radians(deg) if util.nearZero(r - 1.0): return Ideal(rad) elif r < 1.0: return Point(r*math.cos(rad), r*math.sin(rad)) else: raise ValueError('Euclidean coordinates are outside the unit circle') @staticmethod def fromHPolar(hr, theta=None, deg=None): assert (theta is None) != (deg is None) if theta is None: theta = math.radians(deg) r = math.tanh(hr/2) x, y = r*math.cos(theta), r*math.sin(theta) return Point(x, y, hr=hr, theta=theta) def __eq__(self, other): return util.nearZero(self.x - other.x) and util.nearZero(self.y - other.y) def __repr__(self): return '{}({}, {})'.format(type(self).__name__, round(self.x, 3), round(self.y, 3)) def toDrawables(self, elements, radius=0, hradius=None, transform=None, **kwargs): if hradius is not None and not isinstance(self, Ideal): shape = shapes.Circle.fromCenterRadius(Point(self.x, self.y), hradius) return shape.toDrawables(elements, transform=transform, **kwargs) else: x, y = self.x, self.y if transform: x, y = transform.applyToTuple((x, y)) return ECircle(x, y, radius).toDrawables(elements, **kwargs) class Ideal(Point): def __init__(self, theta): self.theta = theta % (2*math.pi) @property def x(self): return math.cos(self.theta) @property def y(self): return math.sin(self.theta) def isIdeal(self): return True @classmethod def fromDegree(cls, deg): return cls(math.radians(deg)) @classmethod def fromRadian(cls, rad): return cls(rad)