circle.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. # Copyright (c) 2010-2019 Manfred Moitzi
  2. # License: MIT License
  3. from typing import TYPE_CHECKING, Sequence
  4. import math
  5. from .line import ConstructionRay
  6. from .vector import Vec2
  7. from .bbox import BoundingBox2d
  8. from .construct2d import ConstructionTool
  9. if TYPE_CHECKING:
  10. from ezdxf.eztypes import Vertex
  11. HALF_PI = math.pi / 2.
  12. class ConstructionCircle(ConstructionTool):
  13. def __init__(self, center: 'Vertex', radius: float = 1.0):
  14. self.center = Vec2(center)
  15. self.radius = float(radius)
  16. if self.radius <= 0.:
  17. raise ValueError("Radius has to be > 0.")
  18. @staticmethod
  19. def from_3p(p1: 'Vertex', p2: 'Vertex', p3: 'Vertex') -> 'ConstructionCircle':
  20. """ Creates a circle from three points. """
  21. p1 = Vec2(p1)
  22. p2 = Vec2(p2)
  23. p3 = Vec2(p3)
  24. ray1 = ConstructionRay(p1, p2)
  25. ray2 = ConstructionRay(p1, p3)
  26. center_ray1 = ray1.orthogonal(p1.lerp(p2))
  27. center_ray2 = ray2.orthogonal(p1.lerp(p3))
  28. center = center_ray1.intersect(center_ray2)
  29. return ConstructionCircle(center, center.distance(p1))
  30. @property
  31. def bounding_box(self) -> 'BoundingBox2d':
  32. rvec = Vec2((self.radius, self.radius))
  33. return BoundingBox2d((self.center - rvec, self.center + rvec))
  34. def move(self, dx: float, dy: float) -> None:
  35. """
  36. Move circle about `dx` in x-axis and about `dy` in y-axis.
  37. Args:
  38. dx: translation in x-axis
  39. dy: translation in y-axis
  40. """
  41. self.center += Vec2((dx, dy))
  42. def point_at(self, angle: float) -> Vec2:
  43. """
  44. Returns point on circle at `angle` as 2d vector.
  45. Args:
  46. angle: angle in radians
  47. """
  48. return self.center + Vec2.from_angle(angle, self.radius)
  49. def inside(self, point: 'Vertex') -> bool:
  50. """ Test if `point` is inside circle. """
  51. return self.radius >= self.center.distance(Vec2(point))
  52. def tangent(self, angle: float) -> ConstructionRay:
  53. """
  54. Returns tangent to circle at `angle` as ConstructionRay().
  55. Args:
  56. angle: angle in radians
  57. """
  58. point_on_circle = self.point_at(angle)
  59. ray = ConstructionRay(self.center, point_on_circle)
  60. return ray.orthogonal(point_on_circle)
  61. def intersect_ray(self, ray: ConstructionRay, abs_tol: float = 1e-12) -> Sequence[Vec2]:
  62. """
  63. Returns intersection points for intersection of this circle with `ray` as sequence of 2d points.
  64. Args:
  65. ray: intersection ray
  66. abs_tol: absolute tolerance for tests (e.g. test for tangents)
  67. Returns: tuple of Vec2()
  68. tuple contains:
  69. 0 points .. no intersection
  70. 1 point .. ray is a tangent on the circle
  71. 2 points .. ray intersects with the circle
  72. """
  73. ortho_ray = ray.orthogonal(self.center)
  74. intersection_point = ray.intersect(ortho_ray)
  75. dist = self.center.distance(intersection_point)
  76. result = []
  77. if dist < self.radius: # intersect in two points
  78. if math.isclose(dist, 0., abs_tol=abs_tol): # if ray goes through center point
  79. angle = ortho_ray.angle
  80. alpha = HALF_PI
  81. else:
  82. # the exact direction of angle (all 4 quadrants Q1-Q4) is important:
  83. # ortho_ray.angle is only at the center point correct
  84. angle = (intersection_point - self.center).angle
  85. alpha = math.acos(intersection_point.distance(self.center) / self.radius)
  86. result.append(self.point_at(angle + alpha))
  87. result.append(self.point_at(angle - alpha))
  88. elif math.isclose(dist, self.radius, abs_tol=abs_tol): # ray is a tangent of circle
  89. result.append(intersection_point)
  90. # else no intersection
  91. return tuple(result)
  92. def intersect_circle(self, other: 'ConstructionCircle', abs_tol: float = 1e-12) -> Sequence[Vec2]:
  93. """
  94. Returns intersection points of two circles as sequence of 2d points.
  95. Args:
  96. other: intersection circle
  97. abs_tol: absolute tolerance for tests (e.g. test for circle touch point)
  98. Returns: tuple of Vec2()
  99. tuple contains:
  100. 0 points .. no intersection
  101. 1 point .. circle touches the other_circle in one point
  102. 2 points .. circle intersects with the other_circle
  103. """
  104. r1 = self.radius
  105. r2 = other.radius
  106. d = self.center.distance(other.center)
  107. d_max = r1 + r2
  108. d_min = math.fabs(r1 - r2)
  109. result = []
  110. if d_min <= d <= d_max:
  111. angle = (other.center - self.center).angle
  112. # if circles touches in one point
  113. if math.isclose(d, d_max, abs_tol=abs_tol) or math.isclose(d, d_min, abs_tol=abs_tol):
  114. result.append(self.point_at(angle))
  115. else: # circles intersect in two points
  116. alpha = math.acos((r2 ** 2 - r1 ** 2 - d ** 2) / (-2. * r1 * d)) # 'Cosinus-Satz'
  117. result.append(self.point_at(angle + alpha))
  118. result.append(self.point_at(angle - alpha))
  119. return tuple(result)