# Created: 13.03.2010 # Copyright (c) 2010, Manfred Moitzi # License: MIT License from typing import TYPE_CHECKING, Optional import math from .construct2d import ConstructionTool from .bbox import BoundingBox2d from .vector import Vec2 if TYPE_CHECKING: from ezdxf.eztypes import Vertex class ParallelRaysError(ArithmeticError): pass HALF_PI = math.pi / 2. THREE_PI_HALF = 1.5 * math.pi DOUBLE_PI = math.pi * 2. class ConstructionRay: """ Infinite construction ray. Args: p1: definition point 1 p2: ray direction as 2nd point or None angle: ray direction as angle in radians or None """ def __init__(self, p1: 'Vertex', p2: 'Vertex' = None, angle: float = None): self._location = Vec2(p1) if p2 is not None: p2 = Vec2(p2) if self._location.x < p2.x: self._direction = (p2 - self._location).normalize() else: self._direction = (self._location - p2).normalize() self._angle = self._direction.angle elif angle is not None: self._angle = angle self._direction = Vec2.from_angle(angle) else: raise ValueError('p2 or angle required.') if math.isclose(self._direction.x, 0., abs_tol=1e-12): self._slope = None self._yof0 = None else: self._slope = self._direction.y / self._direction.x self._yof0 = self._location.y - self._slope * self._location.x self._is_vertical = self._slope is None self._is_horizontal = math.isclose(self._direction.y, 0., abs_tol=1e-12) @property def location(self) -> Vec2: """ Returns location vector of ray. """ return self._location @property def direction(self) -> Vec2: """ Returns direction vector of ray. """ return self._direction @property def slope(self) -> float: """ Returns slope of ray or None if vertical. """ return self._slope @property def angle(self) -> float: """ Returns angle of ray in radians. """ return self._angle @property def angle_deg(self) -> float: """ Returns angle of ray in degrees. """ return math.degrees(self._angle) @property def is_vertical(self) -> bool: """ Returns True if ray is vertical. """ return self._is_vertical @property def is_horizontal(self) -> bool: """ Returns True if ray is horizontal. """ return self._is_horizontal def __str__(self) -> str: return 'ConstructionRay(x={0._x:.3f}, y={0._y:.3f}, phi={0.angle:.5f} rad)'.format(self) def is_parallel(self, other: 'ConstructionRay') -> bool: """ Return True if the rays are parallel, else False. """ if self._is_vertical: return other._is_vertical if other._is_vertical: return False return math.isclose(self._slope, other._slope, abs_tol=1e-12) def intersect(self, other: 'ConstructionRay') -> Vec2: """ Returns the intersection point (xy-tuple) of self and other_ray. Raises: ParallelRaysError: if the rays are parallel """ ray1 = self ray2 = other if not ray1.is_parallel(ray2): if ray1._is_vertical: x = self._location.x y = ray2.yof(x) elif ray2._is_vertical: x = ray2._location.x y = ray1.yof(x) else: # calc intersection with the 'straight-line-equation' # based on y(x) = y0 + x*slope x = (ray1._yof0 - ray2._yof0) / (ray2._slope - ray1._slope) y = ray1.yof(x) return Vec2((x, y)) else: raise ParallelRaysError("Rays are parallel") def orthogonal(self, point: 'Vertex') -> 'ConstructionRay': """ Returns orthogonal construction ray through `point`. """ return ConstructionRay(point, angle=self._angle + HALF_PI) def yof(self, x: float) -> float: if self._is_vertical: raise ArithmeticError return self._yof0 + float(x) * self._slope def xof(self, y: float) -> float: if self._is_vertical: return self._location.x elif not self._is_horizontal: return (float(y) - self._yof0) / self._slope else: raise ArithmeticError def bisectrix(self, other: 'ConstructionRay') -> 'ConstructionRay': """ Bisectrix between self and other construction ray. """ if self.is_parallel(other): raise ParallelRaysError intersection = self.intersect(other) alpha = (self._angle + other._angle) / 2. return ConstructionRay(intersection, angle=alpha) class ConstructionLine(ConstructionTool): """ ConstructionLine is similar to ConstructionRay, but has a start and endpoint and therefor also an direction. The direction goes from start to end, 'left of line' is always in relation to this line direction. """ def __init__(self, start: 'Vertex', end: 'Vertex'): self.start = Vec2(start) self.end = Vec2(end) def __str__(self): return 'ConstructionLine({0.start}, {0.end})'.format(self) # ConstructionTool interface @property def bounding_box(self): return BoundingBox2d((self.start, self.end)) def move(self, dx: float, dy: float) -> None: """ Move line about `dx` in x-axis and about `dy` in y-axis. Args: dx: translation in x-axis dy: translation in y-axis """ v = Vec2((dx, dy)) self.start += v self.end += v @property def sorted_points(self): return (self.end, self.start) if self.start > self.end else (self.start, self.end) @property def ray(self): return ConstructionRay(self.start, self.end) def __eq__(self, other: 'ConstructionLine') -> bool: return self.sorted_points == other.sorted_points def __lt__(self, other: 'ConstructionLine') -> bool: return self.sorted_points < other.sorted_points def length(self) -> float: return (self.end - self.start).magnitude def midpoint(self) -> Vec2: return self.start.lerp(self.end) @property def is_vertical(self) -> bool: return math.isclose(self.start.x, self.end.x) def inside_bounding_box(self, point: 'Vertex') -> bool: return self.bounding_box.inside(point) def intersect(self, other: 'ConstructionLine') -> Optional[Vec2]: """ Returns the intersection point of to lines or None if they have no intersection point. Args: other: other construction line Returns: intersection point or None """ try: point = self.ray.intersect(other.ray) except ParallelRaysError: return None else: if self.inside_bounding_box(point) and other.inside_bounding_box(point): return point else: return None def has_intersection(self, other: 'ConstructionLine') -> bool: # required because intersection Vector(0, 0, 0) is also False return self.intersect(other) is not None def left_of_line(self, point: 'Vertex') -> bool: """ True if `point` is left of construction line in relation to the line direction from start to end. Points exact at the line are not left of the line. """ start, end = self.start, self.end point = Vec2(point) if self.is_vertical: # compute on which site of the line self should be should_be_left = self.start.y < self.end.y if should_be_left: return point.x < self.start.x else: return point.x > self.start.x else: y = self.ray.yof(point.x) # compute if point should be above or below the line should_be_above = start.x < end.x if should_be_above: return point.y > y else: return point.y < y