arc.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. # Copyright (c) 2018 Manfred Moitzi
  2. # License: MIT License
  3. from typing import TYPE_CHECKING, Tuple
  4. from .vector import Vec2
  5. from .bbox import BoundingBox2d
  6. from .construct2d import ConstructionTool, enclosing_angles
  7. from .circle import ConstructionCircle
  8. from .ucs import OCS, UCS
  9. import math
  10. if TYPE_CHECKING:
  11. from ezdxf.eztypes import Vertex, GenericLayoutType
  12. from ezdxf.eztypes import Arc as DXFArc
  13. QUARTER_ANGLES = [0, math.pi * .5, math.pi, math.pi * 1.5]
  14. class ConstructionArc(ConstructionTool):
  15. def __init__(self,
  16. center: 'Vertex' = (0, 0),
  17. radius: float = 1,
  18. start_angle: float = 0,
  19. end_angle: float = 360,
  20. is_counter_clockwise: bool = True):
  21. self.center = Vec2(center)
  22. self.radius = radius
  23. if is_counter_clockwise:
  24. self.start_angle = start_angle
  25. self.end_angle = end_angle
  26. else:
  27. self.start_angle = end_angle
  28. self.end_angle = start_angle
  29. @property
  30. def start_point(self):
  31. return self.center + Vec2.from_deg_angle(self.start_angle, self.radius)
  32. @property
  33. def end_point(self):
  34. return self.center + Vec2.from_deg_angle(self.end_angle, self.radius)
  35. @property
  36. def bounding_box(self) -> 'BoundingBox2d':
  37. bbox = BoundingBox2d((self.start_point, self.end_point))
  38. bbox.extend(self.main_axis_points())
  39. return bbox
  40. def main_axis_points(self):
  41. center = self.center
  42. radius = self.radius
  43. start = math.radians(self.start_angle)
  44. end = math.radians(self.end_angle)
  45. for angle in QUARTER_ANGLES:
  46. if enclosing_angles(angle, start, end):
  47. yield center + Vec2.from_angle(angle, radius)
  48. def move(self, dx: float, dy: float) -> None:
  49. self.center += Vec2((dx, dy))
  50. @property
  51. def start_angle_rad(self) -> float:
  52. return math.radians(self.start_angle)
  53. @property
  54. def end_angle_rad(self) -> float:
  55. return math.radians(self.end_angle)
  56. @staticmethod
  57. def validate_start_and_end_point(start_point: 'Vertex', end_point: 'Vertex') -> Tuple[Vec2, Vec2]:
  58. start_point = Vec2(start_point)
  59. end_point = Vec2(end_point)
  60. if start_point == end_point:
  61. raise ValueError("start- and end point has to be different points.")
  62. return start_point, end_point
  63. @classmethod
  64. def from_2p_angle(cls, start_point: 'Vertex', end_point: 'Vertex', angle: float,
  65. ccw: bool = True) -> 'ConstructionArc':
  66. """
  67. Create arc from two points and enclosing angle. Additional precondition: arc goes by default in counter
  68. clockwise orientation from start_point to end_point, can be changed by ccw=False.
  69. Z-axis of start_point and end_point has to be 0 if given.
  70. Args:
  71. start_point: start point (x, y) as args accepted by Vec2()
  72. end_point: end point (x, y) as args accepted by Vec2()
  73. angle: enclosing angle in degrees
  74. ccw: counter clockwise direction True/False
  75. """
  76. start_point, end_point = cls.validate_start_and_end_point(start_point, end_point)
  77. angle = math.radians(angle)
  78. if angle == 0:
  79. raise ValueError("angle can not be 0.")
  80. if ccw is False:
  81. start_point, end_point = end_point, start_point
  82. alpha2 = angle / 2.
  83. distance = end_point.distance(start_point)
  84. distance2 = distance / 2.
  85. radius = distance2 / math.sin(alpha2)
  86. height = distance2 / math.tan(alpha2)
  87. mid_point = end_point.lerp(start_point, factor=.5)
  88. distance_vector = end_point - start_point
  89. height_vector = distance_vector.orthogonal().normalize(height)
  90. center = mid_point + height_vector
  91. return ConstructionArc(
  92. center=center,
  93. radius=radius,
  94. start_angle=(start_point - center).angle_deg,
  95. end_angle=(end_point - center).angle_deg,
  96. is_counter_clockwise=True,
  97. )
  98. @classmethod
  99. def from_2p_radius(cls, start_point: 'Vertex', end_point: 'Vertex', radius: float, ccw: bool = True,
  100. center_is_left: bool = True) -> 'ConstructionArc':
  101. """
  102. Create arc from two points and arc radius. Additional precondition: arc goes by default in counter clockwise
  103. orientation from start_point to end_point can be changed by ccw=False.
  104. Z-axis of start_point and end_point has to be 0 if given.
  105. The parameter *center_is_left* defines if the center of the arc is left or right of the line *start point* ->
  106. *end point*. Parameter ccw=False swaps start- and end point, which inverts the meaning of center_is_left.
  107. Args:
  108. start_point: start point (x, y) as args accepted by Vec2()
  109. end_point: end point (x, y) as args accepted by Vec2()
  110. radius: arc radius
  111. ccw: counter clockwise direction True/False
  112. center_is_left: center point of arc is left of line SP->EP if True, else on the right side of this line
  113. """
  114. start_point, end_point = cls.validate_start_and_end_point(start_point, end_point)
  115. radius = float(radius)
  116. if radius <= 0:
  117. raise ValueError("radius has to be > 0.")
  118. if ccw is False:
  119. start_point, end_point = end_point, start_point
  120. mid_point = end_point.lerp(start_point, factor=.5)
  121. distance = end_point.distance(start_point)
  122. distance2 = distance / 2.
  123. height = math.sqrt(radius ** 2 - distance2 ** 2)
  124. center = mid_point + (end_point - start_point).orthogonal(ccw=center_is_left).normalize(height)
  125. return ConstructionArc(
  126. center=center,
  127. radius=radius,
  128. start_angle=(start_point - center).angle_deg,
  129. end_angle=(end_point - center).angle_deg,
  130. is_counter_clockwise=True,
  131. )
  132. @classmethod
  133. def from_3p(cls, start_point: 'Vertex', end_point: 'Vertex', def_point: 'Vertex',
  134. ccw: bool = True) -> 'ConstructionArc':
  135. """
  136. Create arc from three points. Additional precondition: arc goes in counter clockwise
  137. orientation from start_point to end_point. Z-axis of start_point, end_point and def_point has to be 0 if given.
  138. Args:
  139. start_point: start point (x, y) as args accepted by Vec2()
  140. end_point: end point (x, y) as args accepted by Vec2()
  141. def_point: additional definition point as (x, y) as args accepted by Vec2()
  142. ccw: counter clockwise direction True/False
  143. """
  144. start_point, end_point = cls.validate_start_and_end_point(start_point, end_point)
  145. def_point = Vec2(def_point)
  146. if def_point == start_point or def_point == end_point:
  147. raise ValueError("def point has to be different to start- and end point")
  148. circle = ConstructionCircle.from_3p(start_point, end_point, def_point)
  149. center = Vec2(circle.center)
  150. return ConstructionArc(
  151. center=center,
  152. radius=circle.radius,
  153. start_angle=(start_point - center).angle_deg,
  154. end_angle=(end_point - center).angle_deg,
  155. is_counter_clockwise=ccw,
  156. )
  157. def add_to_layout(self, layout: 'GenericLayoutType', ucs: UCS = None, dxfattribs: dict = None) -> 'DXFArc':
  158. """
  159. Add arc as DXF entity to a layout.
  160. Supports 3D arcs by using an UCS. An ConstructionArc is always defined in the xy-plane, but by using an arbitrary UCS, the
  161. arc can be placed in 3D space, automatically OCS transformation included.
  162. Args:
  163. layout: destination layout (model space, paper space or block)
  164. ucs: arc properties transformation from ucs to ocs
  165. dxfattribs: usual DXF attributes supported by ARC
  166. Returns: DXF ConstructionArc() object
  167. """
  168. if ucs is not None:
  169. if dxfattribs is None:
  170. dxfattribs = {}
  171. dxfattribs['extrusion'] = ucs.uz
  172. ocs = OCS(ucs.uz)
  173. wcs_center = ucs.to_wcs(self.center)
  174. ocs_center = ocs.from_wcs(wcs_center)
  175. arc = self.__class__(radius=self.radius)
  176. arc.center = ocs_center
  177. arc.start_angle = ucs.to_ocs_angle_deg(self.start_angle)
  178. arc.end_angle = ucs.to_ocs_angle_deg(self.end_angle)
  179. else:
  180. arc = self
  181. return layout.add_arc(
  182. center=arc.center,
  183. radius=arc.radius,
  184. start_angle=arc.start_angle,
  185. end_angle=arc.end_angle,
  186. dxfattribs=dxfattribs,
  187. )