Files
stepanalyser/.venv/lib/python3.12/site-packages/ezdxf/math/circle.py
Christian Anetzberger a197de9456 initial
2026-01-22 20:23:51 +01:00

250 lines
8.7 KiB
Python

# Copyright (c) 2010-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import Sequence, Iterator, Iterable
import math
import numpy as np
from ezdxf.math import Vec2, UVec
from .line import ConstructionRay, ConstructionLine
from .bbox import BoundingBox2d
HALF_PI = math.pi / 2.0
__all__ = ["ConstructionCircle"]
class ConstructionCircle:
"""Construction tool for 2D circles.
Args:
center: center point as :class:`Vec2` compatible object
radius: circle radius > `0`
"""
def __init__(self, center: UVec, radius: float = 1.0):
self.center = Vec2(center)
self.radius = float(radius)
if self.radius <= 0.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: UVec, p2: UVec, p3: UVec) -> 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, angle goes counter
clockwise around the z-axis, x-axis = 0 deg.
"""
return self.center + Vec2.from_angle(angle, self.radius)
def vertices(self, angles: Iterable[float]) -> Iterable[Vec2]:
"""Yields vertices of the circle for iterable `angles`.
Args:
angles: iterable of angles as radians, angle goes counter-clockwise
around the z-axis, x-axis = 0 deg.
"""
center = self.center
radius = self.radius
for angle in angles:
yield center + Vec2.from_angle(angle, radius)
def flattening(self, sagitta: float) -> Iterator[Vec2]:
"""Approximate the circle by vertices, argument `sagitta` is the
max. distance from the center of an arc segment to the center of its
chord. Returns a closed polygon where the start vertex is coincident
with the end vertex!
"""
from .arc import arc_segment_count
count = arc_segment_count(self.radius, math.tau, sagitta)
yield from self.vertices(np.linspace(0.0, math.tau, count + 1))
def inside(self, point: UVec) -> 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
=========== ==================================
"""
assert isinstance(ray, ConstructionRay)
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.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_line(
self, line: ConstructionLine, abs_tol: float = 1e-10
) -> Sequence[Vec2]:
"""Returns intersection points of circle and `line` as sequence of
:class:`Vec2` objects.
Args:
line: intersection line
abs_tol: absolute tolerance for tests (e.g. test for tangents)
Returns:
tuple of :class:`Vec2` objects
=========== ==================================
tuple size Description
=========== ==================================
0 no intersection
1 line intersects or touches the circle at one point
2 line intersects the circle at two points
=========== ==================================
"""
assert isinstance(line, ConstructionLine)
return [
point
for point in self.intersect_ray(line.ray, abs_tol=abs_tol)
if is_point_in_line_range(line.start, line.end, point)
]
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
=========== ==================================
"""
assert isinstance(other, ConstructionCircle)
r1 = self.radius
r2 = other.radius
d = self.center.distance(other.center)
if d < abs_tol:
# concentric circles do not intersect by definition
return tuple()
d_max = r1 + r2
d_min = math.fabs(r1 - r2)
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):
return (self.point_at(angle),)
if math.isclose(d, d_min, abs_tol=abs_tol):
if r1 >= r2:
return (self.point_at(angle),)
return (self.point_at(angle + math.pi),)
else: # Circles intersect in two points:
# Law of Cosines:
alpha = math.acos((r2 * r2 - r1 * r1 - d * d) / (-2.0 * r1 * d))
return tuple(self.vertices((angle + alpha, angle - alpha)))
return tuple()
def is_point_in_line_range(start: Vec2, end: Vec2, point: Vec2) -> bool:
length = (end - start).magnitude
if (point - start).magnitude > length:
return False
return (point - end).magnitude <= length