import math from .. import util from ..euclid.shapes import Circle, Arc, Line as ELine, OriginLine from ..euclid import intersection from ..euclid.intersection import InfiniteIntersections, SingleIntersection, \ NoIntersection from .shapes import Point from . import shapes as poincare class Hypercycle: def __init__(self, projShape, segment=False): assert isinstance(projShape, (Circle, Arc, ELine, OriginLine)) if segment and not isinstance(projShape, (Arc, ELine)): raise ValueError('Not enough information to determine segment endpoints') unit = Circle(0,0,1) if segment: if isinstance(projShape, Arc): # TODO: Trim if arc extends outside unit circle pass elif isinstance(projShape, ELine): # TODO: Trim if arc extends outside unit circle pass else: raise ValueError('Unknown type') else: if isinstance(projShape, Circle): x1, y1, x2, y2 = intersection.circleCircle(projShape, unit) if projShape.cw: x1, y1, x2, y2 = x2, y2, x1, y1 projShape = Arc.fromPointsWithCenter(x1, y1, x2, y2, projShape.cx, projShape.cy, r=projShape.r, cw=projShape.cw) elif isinstance(projShape, ELine): x1, y1, x2, y2 = intersection.circleLine(unit, projShape) # Keep direction of line the same origAng = math.atan2(projShape.y2-projShape.y1, projShape.x2-projShape.x1) newAng = math.atan2(y2-y1, x2-x1) angDiff = (newAng - origAng) % (math.pi*2) if math.pi/2 < angDiff < 3*math.pi/2: x1, y1, x2, y2 = x2, y2, x1, y1 projShape = ELine(x1, y1, x2, y2) else: raise ValueError('Unknown type') self.projShape = projShape self.segment = segment def startPoint(self): return Point(*self.projShape.startPoint()) def endPoint(self): return Point(*self.projShape.endPoint()) def intersectionsWithHcycle(self, hcycle2): ''' Returns list of intersections in the unit circle ''' x2, y2 = 1, 1 # Default, invalid try: if isinstance(self.projShape, Circle): if isinstance(hcycle2.projShape, Circle): x1, y1, x2, y2 = intersection.circleCircle( self.projShape, hcycle2.projShape) else: x1, y1, x2, y2 = intersection.circleLine( self.projShape, hcycle2.projShape) else: if isinstance(hcycle2.projShape, Circle): x1, y1, x2, y2 = intersection.circleLine( hcycle2.projShape, self.projShape) else: x1, y1 = intersection.lineLine( self.projShape, hcycle2.projShape) except InfiniteIntersections as e: raise e from None except SingleIntersection as e: x1, y1 = e.args except NoIntersection: x1, y1 = 1, 1 # Invalid pts = [] try: pts.append(Point.fromEuclid(x1, y1)) except ValueError: pass try: pts.append(Point.fromEuclid(x2, y2)) except ValueError: pass return pts def trimmed(self, x1, y1, x2, y2, **kwargs): ''' Returns a segment of this hypercycle going from x1,y1 to x2,y2 (assuming that x1,y1 and x2,y2 are on the hypercycle) ''' trimmedShape = self.projShape.trimmed(x1, y1, x2, y2, **kwargs) return type(self)(trimmedShape, segment=True) def midpointEuclid(self): x, y = self.projShape.midpoint() return Point(x, y) def reverse(self): self.projShape.reverse() def reversed(self): newShape = self.projShape.reversed() newObj = type(self)(newShape, segment=True) newObj.segment = self.segment return newObj def makePerpendicular(self, x, y): if util.nearZero(x) and util.nearZero(y): radical1 = None else: radical1 = ELine.radicalAxis(Circle(0,0,1), (x,y)) radical2 = ELine.radicalAxis(self.projShape, (x,y)) if radical1 is None or radical1.parallelTo(radical2): # Infinite radius circle shape = radical2.makePerpendicular(x,y) # Ensure correct direction if isinstance(self.projShape, ELine): if shape.antiparallelTo(self.projShape): shape.reverse() else: # projShape is a Circle radial = ELine.fromPoints(x, y, self.projShape.cx, self.projShape.cy) if radial.antiparallelTo(shape) ^ self.projShape.cw: shape.reverse() return poincare.Line(shape, segment=False) else: cx, cy = intersection.lineLine(radical1, radical2) r = math.hypot(cy-y, cx-x) shape = Circle(cx, cy, r) # Ensure correct direction shape.cw = False if isinstance(self.projShape, ELine): x1, y1, x2, y2 = intersection.lineCircle(self.projShape, shape) else: # projShape is a Circle x1, y1, x2, y2 = intersection.circleCircle(self.projShape, shape) shape.cw = shape.cw ^ self.projShape.cw if math.hypot(x1, y1) <= math.hypot(x2, y2): shape.cw = not shape.cw return poincare.Line(shape, segment=False) def makeCap(self, point): if point.isIdeal(): return point else: return self.makePerpendicular(*point) def makeOffset(self, offset): return Hypercycle.fromHypercycleOffset(self, offset) @classmethod def fromHypercycleOffset(cls, hcycle, offset, unit=Circle(0,0,1)): dh = offset #dh = math.asinh(offset/2) if isinstance(hcycle.projShape, Circle): # May throw if bad geometry x1, y1, x2, y2 = intersection.circleCircle(hcycle.projShape, unit) cx, cy = hcycle.projShape.cx, hcycle.projShape.cy r, cw = hcycle.projShape.r, hcycle.projShape.cw if cw: x1, y1, x2, y2 = x2, y2, x1, y1 rc = math.hypot(cx, cy) deMid = rc - r dhMid = 2 * math.atanh(deMid) sign = -1 if cw else 1 t = math.tanh((dhMid + sign*dh)/2) xOff, yOff = t * cx / rc, t * cy / rc else: # May throw if bad geometry x1, y1, x2, y2 = intersection.lineCircle(hcycle.projShape, unit) lineAng = math.atan2(y2-y1, x2-x1) mx, my = (x1+x2)/2, (y1+y2)/2 # Midpoint if util.nearZero(mx) and util.nearZero(my): ang = math.atan2(y2-y1, x2-x1) + math.pi/2 else: ang = math.atan2(my, mx) # Test if the line goes clockwise around the origin cw = (ang - lineAng) % (math.pi*2) < math.pi rm = math.hypot(mx, my) deMid = rm dhMid = 2 * math.atanh(deMid) sign = 1 if cw else -1 t = math.tanh((dhMid + sign*dh)/2) xOff, yOff = t * math.cos(ang), t * math.sin(ang) return cls.fromPoints(x1, y1, x2, y2, xOff, yOff, segment=False, excludeMid=False) @staticmethod def _pointsInlineWithOrigin(x1, y1, x2, y2): if ((util.nearZero(x1) and util.nearZero(x2)) or (util.nearZero(y1) and util.nearZero(y2)) or (util.nearZero(x1) and util.nearZero(y1)) or (util.nearZero(x2) and util.nearZero(y2))): return True elif ((util.nearZero(x1) or util.nearZero(x2)) and (util.nearZero(y1) or util.nearZero(y2))): return False elif util.nearZero(x1) or util.nearZero(x2): return util.nearZero(x1/y1 - x2/y2) else: return util.nearZero(y1/x1 - y2/x2) @classmethod def fromPoints(cls, sx, sy, ex, ey, mx, my, segment=False, excludeMid=False, **kwargs): ''' Start xy, end xy, other xy; pass excludeMid=True if other point is not between start and end ''' if util.nearZero(sx-ex) and util.nearZero(sy-ey): assert False, 'Start and end points are the same' if util.nearZero(sx-mx) and util.nearZero(sy-my): assert False, 'Start and mid points are the same' if util.nearZero(mx-ex) and util.nearZero(my-ey): assert False, 'Mid and end points are the same' if cls._pointsInlineWithOrigin(sx-mx, sy-my, ex-mx, ey-my): shape = ELine(sx, sy, ex, ey) else: shape = Arc.fromPoints(sx, sy, ex, ey, mx, my, excludeMid=excludeMid) return cls(shape, segment=segment, **kwargs) def toDrawables(self, elements, hwidth=None, transform=None, **kwargs): if hwidth is not None: try: hwidth1, hwidth2 = hwidth except TypeError: hwidth = float(hwidth) hwidth1, hwidth2 = hwidth/2, -hwidth/2 if self.segment: edges = [] edges.append(self.makeOffset(hwidth1)) edges.append(self.makeCap(self.startPoint())) edges.append(self.makeOffset(hwidth2)) edges.append(self.makeCap(self.endPoint())) poly = poincare.Polygon.fromEdges(edges, join=True) return poly.toDrawables(elements, transform=transform, **kwargs) else: edge1 = Hypercycle.fromHypercycleOffset(self, hwidth1).projShape edge2 = Hypercycle.fromHypercycleOffset(self, hwidth2).projShape edge2.reverse() path = elements.Path(**kwargs) if transform: edge1 = transform.applyToShape(edge1) edge2 = transform.applyToShape(edge2) edge1.drawToPath(path, includeM=True) edge2.drawToPath(path, includeM=False) path.Z() return (path,) else: shape = self.projShape if transform: shape = transform.applyToShape(shape) return shape.toDrawables(elements, **kwargs) def drawToPath(self, path, transform=None, **kwargs): shape = self.projShape if transform: shape = transform.applyToShape(shape) return shape.drawToPath(path, **kwargs)