File: circle.py

package info (click to toggle)
ezdxf 0.14.2-3
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 24,920 kB
  • sloc: python: 75,150; javascript: 166; makefile: 115
file content (176 lines) | stat: -rw-r--r-- 6,287 bytes parent folder | download
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)