1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
|
# Copyright (c) 2010-2020 Manfred Moitzi
# License: MIT License
from typing import TYPE_CHECKING, Sequence
import math
from .line import ConstructionRay
from .vector import Vec2
from .bbox import BoundingBox2d
if TYPE_CHECKING:
from ezdxf.eztypes import Vertex
HALF_PI = math.pi / 2.
class ConstructionCircle:
""" Circle construction tool.
Args:
center: center point as :class:`Vec2` compatible object
radius: circle radius > `0`
"""
def __init__(self, center: 'Vertex', radius: float = 1.0):
self.center = Vec2(center)
self.radius = float(radius)
if self.radius <= 0.:
raise ValueError("Radius has to be > 0.")
def __str__(self) -> str:
""" Returns string representation of circle
"ConstructionCircle(center, radius)".
"""
return f'ConstructionCircle({self.center}, {self.radius})'
@staticmethod
def from_3p(p1: 'Vertex', p2: 'Vertex',
p3: 'Vertex') -> 'ConstructionCircle':
""" Creates a circle from three points, all points have to be compatible
to :class:`Vec2` class.
"""
p1 = Vec2(p1)
p2 = Vec2(p2)
p3 = Vec2(p3)
ray1 = ConstructionRay(p1, p2)
ray2 = ConstructionRay(p1, p3)
center_ray1 = ray1.orthogonal(p1.lerp(p2))
center_ray2 = ray2.orthogonal(p1.lerp(p3))
center = center_ray1.intersect(center_ray2)
return ConstructionCircle(center, center.distance(p1))
@property
def bounding_box(self) -> 'BoundingBox2d':
""" 2D bounding box of circle as :class:`BoundingBox2d` object. """
rvec = Vec2((self.radius, self.radius))
return BoundingBox2d((self.center - rvec, self.center + rvec))
def translate(self, dx: float, dy: float) -> None:
""" Move circle about `dx` in x-axis and about `dy` in y-axis.
Args:
dx: translation in x-axis
dy: translation in y-axis
"""
self.center += Vec2((dx, dy))
def point_at(self, angle: float) -> Vec2:
""" Returns point on circle at `angle` as :class:`Vec2` object.
Args:
angle: angle in radians
"""
return self.center + Vec2.from_angle(angle, self.radius)
def inside(self, point: 'Vertex') -> bool:
""" Returns ``True`` if `point` is inside circle. """
return self.radius >= self.center.distance(Vec2(point))
def tangent(self, angle: float) -> ConstructionRay:
""" Returns tangent to circle at `angle` as :class:`ConstructionRay`
object.
Args:
angle: angle in radians
"""
point_on_circle = self.point_at(angle)
ray = ConstructionRay(self.center, point_on_circle)
return ray.orthogonal(point_on_circle)
def intersect_ray(self, ray: ConstructionRay,
abs_tol: float = 1e-10) -> Sequence[Vec2]:
""" Returns intersection points of circle and `ray` as sequence of
:class:`Vec2` objects.
Args:
ray: intersection ray
abs_tol: absolute tolerance for tests (e.g. test for tangents)
Returns:
tuple of :class:`Vec2` objects
=========== ==================================
tuple size Description
=========== ==================================
0 no intersection
1 ray is a tangent to circle
2 ray intersects with the circle
=========== ==================================
"""
ortho_ray = ray.orthogonal(self.center)
intersection_point = ray.intersect(ortho_ray)
dist = self.center.distance(intersection_point)
result = []
# Intersect in two points:
if dist < self.radius:
# Ray goes through center point:
if math.isclose(dist, 0., abs_tol=abs_tol):
angle = ortho_ray.angle
alpha = HALF_PI
else:
# The exact direction of angle (all 4 quadrants Q1-Q4) is
# important: ortho_ray.angle is only correct at the center point
angle = (intersection_point - self.center).angle
alpha = math.acos(intersection_point.distance(
self.center) / self.radius)
result.append(self.point_at(angle + alpha))
result.append(self.point_at(angle - alpha))
# Ray is a tangent of the circle:
elif math.isclose(dist, self.radius, abs_tol=abs_tol):
result.append(intersection_point)
# else: No intersection
return tuple(result)
def intersect_circle(self, other: 'ConstructionCircle',
abs_tol: float = 1e-10) -> Sequence[Vec2]:
""" Returns intersection points of two circles as sequence of
:class:`Vec2` objects.
Args:
other: intersection circle
abs_tol: absolute tolerance for tests
Returns:
tuple of :class:`Vec2` objects
=========== ==================================
tuple size Description
=========== ==================================
0 no intersection
1 circle touches the `other` circle at one point
2 circle intersects with the `other` circle
=========== ==================================
"""
r1 = self.radius
r2 = other.radius
d = self.center.distance(other.center)
d_max = r1 + r2
d_min = math.fabs(r1 - r2)
result = []
if d_min <= d <= d_max:
angle = (other.center - self.center).angle
# Circles touches at one point:
if math.isclose(d, d_max, abs_tol=abs_tol) or math.isclose(
d, d_min, abs_tol=abs_tol):
result.append(self.point_at(angle))
else: # Circles intersect in two points:
# Law of Cosines:
alpha = math.acos((r2 ** 2 - r1 ** 2 - d ** 2) / (-2. * r1 * d))
result.append(self.point_at(angle + alpha))
result.append(self.point_at(angle - alpha))
return tuple(result)
|