line.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. # Created: 13.03.2010
  2. # Copyright (c) 2010, Manfred Moitzi
  3. # License: MIT License
  4. from typing import TYPE_CHECKING, Optional
  5. import math
  6. from .construct2d import ConstructionTool
  7. from .bbox import BoundingBox2d
  8. from .vector import Vec2
  9. if TYPE_CHECKING:
  10. from ezdxf.eztypes import Vertex
  11. class ParallelRaysError(ArithmeticError):
  12. pass
  13. HALF_PI = math.pi / 2.
  14. THREE_PI_HALF = 1.5 * math.pi
  15. DOUBLE_PI = math.pi * 2.
  16. class ConstructionRay:
  17. """
  18. Infinite construction ray.
  19. Args:
  20. p1: definition point 1
  21. p2: ray direction as 2nd point or None
  22. angle: ray direction as angle in radians or None
  23. """
  24. def __init__(self, p1: 'Vertex', p2: 'Vertex' = None, angle: float = None):
  25. self._location = Vec2(p1)
  26. if p2 is not None:
  27. p2 = Vec2(p2)
  28. if self._location.x < p2.x:
  29. self._direction = (p2 - self._location).normalize()
  30. else:
  31. self._direction = (self._location - p2).normalize()
  32. self._angle = self._direction.angle
  33. elif angle is not None:
  34. self._angle = angle
  35. self._direction = Vec2.from_angle(angle)
  36. else:
  37. raise ValueError('p2 or angle required.')
  38. if math.isclose(self._direction.x, 0., abs_tol=1e-12):
  39. self._slope = None
  40. self._yof0 = None
  41. else:
  42. self._slope = self._direction.y / self._direction.x
  43. self._yof0 = self._location.y - self._slope * self._location.x
  44. self._is_vertical = self._slope is None
  45. self._is_horizontal = math.isclose(self._direction.y, 0., abs_tol=1e-12)
  46. @property
  47. def location(self) -> Vec2:
  48. """ Returns location vector of ray. """
  49. return self._location
  50. @property
  51. def direction(self) -> Vec2:
  52. """ Returns direction vector of ray. """
  53. return self._direction
  54. @property
  55. def slope(self) -> float:
  56. """ Returns slope of ray or None if vertical. """
  57. return self._slope
  58. @property
  59. def angle(self) -> float:
  60. """ Returns angle of ray in radians. """
  61. return self._angle
  62. @property
  63. def angle_deg(self) -> float:
  64. """ Returns angle of ray in degrees. """
  65. return math.degrees(self._angle)
  66. @property
  67. def is_vertical(self) -> bool:
  68. """ Returns True if ray is vertical. """
  69. return self._is_vertical
  70. @property
  71. def is_horizontal(self) -> bool:
  72. """ Returns True if ray is horizontal. """
  73. return self._is_horizontal
  74. def __str__(self) -> str:
  75. return 'ConstructionRay(x={0._x:.3f}, y={0._y:.3f}, phi={0.angle:.5f} rad)'.format(self)
  76. def is_parallel(self, other: 'ConstructionRay') -> bool:
  77. """
  78. Return True if the rays are parallel, else False.
  79. """
  80. if self._is_vertical:
  81. return other._is_vertical
  82. if other._is_vertical:
  83. return False
  84. return math.isclose(self._slope, other._slope, abs_tol=1e-12)
  85. def intersect(self, other: 'ConstructionRay') -> Vec2:
  86. """
  87. Returns the intersection point (xy-tuple) of self and other_ray.
  88. Raises:
  89. ParallelRaysError: if the rays are parallel
  90. """
  91. ray1 = self
  92. ray2 = other
  93. if not ray1.is_parallel(ray2):
  94. if ray1._is_vertical:
  95. x = self._location.x
  96. y = ray2.yof(x)
  97. elif ray2._is_vertical:
  98. x = ray2._location.x
  99. y = ray1.yof(x)
  100. else:
  101. # calc intersection with the 'straight-line-equation'
  102. # based on y(x) = y0 + x*slope
  103. x = (ray1._yof0 - ray2._yof0) / (ray2._slope - ray1._slope)
  104. y = ray1.yof(x)
  105. return Vec2((x, y))
  106. else:
  107. raise ParallelRaysError("Rays are parallel")
  108. def orthogonal(self, point: 'Vertex') -> 'ConstructionRay':
  109. """
  110. Returns orthogonal construction ray through `point`.
  111. """
  112. return ConstructionRay(point, angle=self._angle + HALF_PI)
  113. def yof(self, x: float) -> float:
  114. if self._is_vertical:
  115. raise ArithmeticError
  116. return self._yof0 + float(x) * self._slope
  117. def xof(self, y: float) -> float:
  118. if self._is_vertical:
  119. return self._location.x
  120. elif not self._is_horizontal:
  121. return (float(y) - self._yof0) / self._slope
  122. else:
  123. raise ArithmeticError
  124. def bisectrix(self, other: 'ConstructionRay') -> 'ConstructionRay':
  125. """
  126. Bisectrix between self and other construction ray.
  127. """
  128. if self.is_parallel(other):
  129. raise ParallelRaysError
  130. intersection = self.intersect(other)
  131. alpha = (self._angle + other._angle) / 2.
  132. return ConstructionRay(intersection, angle=alpha)
  133. class ConstructionLine(ConstructionTool):
  134. """
  135. ConstructionLine is similar to ConstructionRay, but has a start and endpoint and therefor also an direction.
  136. The direction goes from start to end, 'left of line' is always in relation to this line direction.
  137. """
  138. def __init__(self, start: 'Vertex', end: 'Vertex'):
  139. self.start = Vec2(start)
  140. self.end = Vec2(end)
  141. def __str__(self):
  142. return 'ConstructionLine({0.start}, {0.end})'.format(self)
  143. # ConstructionTool interface
  144. @property
  145. def bounding_box(self):
  146. return BoundingBox2d((self.start, self.end))
  147. def move(self, dx: float, dy: float) -> None:
  148. """
  149. Move line about `dx` in x-axis and about `dy` in y-axis.
  150. Args:
  151. dx: translation in x-axis
  152. dy: translation in y-axis
  153. """
  154. v = Vec2((dx, dy))
  155. self.start += v
  156. self.end += v
  157. @property
  158. def sorted_points(self):
  159. return (self.end, self.start) if self.start > self.end else (self.start, self.end)
  160. @property
  161. def ray(self):
  162. return ConstructionRay(self.start, self.end)
  163. def __eq__(self, other: 'ConstructionLine') -> bool:
  164. return self.sorted_points == other.sorted_points
  165. def __lt__(self, other: 'ConstructionLine') -> bool:
  166. return self.sorted_points < other.sorted_points
  167. def length(self) -> float:
  168. return (self.end - self.start).magnitude
  169. def midpoint(self) -> Vec2:
  170. return self.start.lerp(self.end)
  171. @property
  172. def is_vertical(self) -> bool:
  173. return math.isclose(self.start.x, self.end.x)
  174. def inside_bounding_box(self, point: 'Vertex') -> bool:
  175. return self.bounding_box.inside(point)
  176. def intersect(self, other: 'ConstructionLine') -> Optional[Vec2]:
  177. """
  178. Returns the intersection point of to lines or None if they have no intersection point.
  179. Args:
  180. other: other construction line
  181. Returns: intersection point or None
  182. """
  183. try:
  184. point = self.ray.intersect(other.ray)
  185. except ParallelRaysError:
  186. return None
  187. else:
  188. if self.inside_bounding_box(point) and other.inside_bounding_box(point):
  189. return point
  190. else:
  191. return None
  192. def has_intersection(self, other: 'ConstructionLine') -> bool:
  193. # required because intersection Vector(0, 0, 0) is also False
  194. return self.intersect(other) is not None
  195. def left_of_line(self, point: 'Vertex') -> bool:
  196. """
  197. True if `point` is left of construction line in relation to the line direction from start to end.
  198. Points exact at the line are not left of the line.
  199. """
  200. start, end = self.start, self.end
  201. point = Vec2(point)
  202. if self.is_vertical:
  203. # compute on which site of the line self should be
  204. should_be_left = self.start.y < self.end.y
  205. if should_be_left:
  206. return point.x < self.start.x
  207. else:
  208. return point.x > self.start.x
  209. else:
  210. y = self.ray.yof(point.x)
  211. # compute if point should be above or below the line
  212. should_be_above = start.x < end.x
  213. if should_be_above:
  214. return point.y > y
  215. else:
  216. return point.y < y