Source code for simetri.geometry.ellipse

'''Functions for working with ellipses.'''

from math import cos, sin, pi, atan2, sqrt, degrees

from scipy.special import ellipeinc
import numpy as np

from ..graphics.shape import Shape, custom_attributes
from ..graphics.common import Point
from ..graphics.all_enums import Types
from ..settings.settings import defaults
from .geometry import distance, positive_angle


ndarray = np.ndarray

[docs] class Arc(Shape): """A circular or elliptic arc defined by a center, radius, radius2, start angle, and end angle. If radius2 is not provided, the arc is a circular arc.""" def __init__( self, center: Point, start_angle: float, span_angle: float, radius: float, radius2: float, n_points: int = None, rot_angle: float = 0, xform_matrix: ndarray = None, **kwargs, ): """ Args: center (Point): The center of the arc. start_angle (float): The starting angle of the arc. span_angle (float): The span angle of the arc. radius (float): The radius of the arc. radius2 (float): The second radius for elliptical arcs. n_points (int, optional): Number of points to generate. Defaults to None. rot_angle (float, optional): Rotation angle. Defaults to 0. xform_matrix (ndarray, optional): Transformation matrix. Defaults to None. **kwargs: Additional keyword arguments. """ if n_points is None: n_points = defaults["n_arc_points"] vertices = [tuple(p) for p in elliptic_arc_points(center, radius, radius2, start_angle, span_angle, n_points)] super().__init__(vertices, subtype=Types.ARC, xform_matrix=xform_matrix, **kwargs) self.center = center self.radius = radius self.radius2 = radius2 self.start_angle = start_angle self.span_angle = span_angle self.start_point = vertices[0] self.end_point = vertices[-1] self.rot_angle = rot_angle
[docs] def arc2(start, start_angle, end_angle, radius1, radius2): '''Return the vertices of an elliptic arc. Args: start (tuple): Starting point of the arc. start_angle (float): Starting angle of the arc. end_angle (float): Ending angle of the arc. radius1 (float): First radius of the arc. radius2 (float): Second radius of the arc. Returns: list: Vertices of the elliptic arc. ''' center = (0, 0)
[docs] class Ellipse(Shape): """An ellipse defined by center, width, and height.""" def __init__(self, center: Point, width: float, height: float, angle:float=0, xform_matrix:ndarray = None, **kwargs) -> None: """ Args: center (Point): The center of the ellipse. width (float): The width of the ellipse. height (float): The height of the ellipse. angle (float, optional): Rotation angle. Defaults to 0. xform_matrix (ndarray, optional): Transformation matrix. Defaults to None. **kwargs: Additional keyword arguments. """ n_points = defaults["n_ellipse_points"] vertices = [tuple(p) for p in ellipse_points(center, width / 2, height / 2, n_points)] super().__init__(vertices, closed=True, xform_matrix=xform_matrix, **kwargs) a = width / 2 b = height / 2 self.a = a self.b = b self.center = center self.width = width self.height = height self.angle = angle self.smooth = True self.closed = True self.subtype = Types.ELLIPSE @property def closed(self): """Return True ellipse is always closed. Returns: bool: Always returns True. """ return True @closed.setter def closed(self, value: bool): pass
[docs] def copy(self): """Return a copy of the ellipse. Returns: Ellipse: A copy of the ellipse. """ center = self.center width = self.width height = self.height ellipse = Ellipse(center, width, height) # ellipse.style = self.style.copy() custom_attribs = custom_attributes(self) for attrib in custom_attribs: setattr(ellipse, attrib, getattr(self, attrib)) return ellipse
[docs] def ellipse_tangent(a, b, x, y, tol=.001): """Calculates the angle of the tangent line to an ellipse at the point (x, y). Args: a (float): Semi-major axis of the ellipse. b (float): Semi-minor axis of the ellipse. x (float): x-coordinate of the point. y (float): y-coordinate of the point. tol (float, optional): Tolerance for point on ellipse check. Defaults to .001. Returns: float: Angle of the tangent line in radians. """ if abs((x**2 / a**2) + (y**2 / b**2) - 1) >= tol: res = False else: # res = atan2(-(b**2 * x), (a**2 * y)) res = atan2((b**2 * x), -(a**2 * y)) return res
[docs] def r_central(a, b, theta): '''Return the radius (distance between the center and the intersection point) of the ellipse at the given angle. Args: a (float): Semi-major axis of the ellipse. b (float): Semi-minor axis of the ellipse. theta (float): Angle in radians. Returns: float: Radius at the given angle. ''' return (a * b) / sqrt((b * cos(theta))**2 + (a * sin(theta))**2)
[docs] def ellipse_line_intersection(a, b, point): '''Return the intersection points of an ellipse and a line segment connecting the given point to the ellipse center at (0, 0). Args: a (float): Semi-major axis of the ellipse. b (float): Semi-minor axis of the ellipse. point (tuple): Point coordinates (x, y). Returns: list: Intersection points. ''' # adapted from http://mathworld.wolfram.com/Ellipse-LineIntersection.html # a, b is the ellipse width/2 and height/2 and (x_0, y_0) is the point x_0, y_0 = point[:2] x = ((a * b) / (sqrt(a**2 * y_0**2 + b**2 * x_0**2))) * x_0 y = ((a * b) / (sqrt(a**2 * y_0**2 + b**2 * x_0**2))) * y_0 return [(x, y), (-x, -y)]
[docs] def elliptic_arc_points(center, rx, ry, start_angle, span_angle, num_points): """Generate points on an elliptic arc. These are generated from the parametric equations of the ellipse. They are not evenly spaced. Args: center (tuple): (x, y) coordinates of the ellipse center. rx (float): Length of the semi-major axis. ry (float): Length of the semi-minor axis. start_angle (float): Starting angle of the arc. span_angle (float): Span angle of the arc. num_points (int): Number of points to generate. Returns: numpy.ndarray: Array of (x, y) coordinates of the ellipse points. """ start_angle = positive_angle(start_angle) clockwise = span_angle < 0 if clockwise: if start_angle + span_angle < 0: end_angle = positive_angle(start_angle + span_angle) t0 = get_ellipse_t_for_angle(end_angle, rx, ry) t1 = get_ellipse_t_for_angle(2*pi, rx, ry) t = np.linspace(t0, t1, num_points) x = center[0] + rx * np.cos(t) y = center[1] + ry * np.sin(t) slice1 = np.column_stack((x, y)) t0 = get_ellipse_t_for_angle(0, rx, ry) t1 = get_ellipse_t_for_angle(start_angle, rx, ry) t = np.linspace(t0, t1, num_points) x = center[0] + rx * np.cos(t) y = center[1] + ry * np.sin(t) slice2 = np.column_stack((x, y)) res = np.flip(np.concatenate((slice1, slice2)), axis=0) else: end_angle = start_angle + span_angle t0 = get_ellipse_t_for_angle(end_angle, rx, ry) t1 = get_ellipse_t_for_angle(start_angle, rx, ry) t = np.linspace(t0, t1, num_points) x = center[0] + rx * np.cos(t) y = center[1] + ry * np.sin(t) res = np.flip(np.column_stack((x, y)), axis=0) else: if start_angle + span_angle > 2*pi: t0 = get_ellipse_t_for_angle(start_angle, rx, ry) t1 = get_ellipse_t_for_angle(2*pi, rx, ry) t = np.linspace(t0, t1, num_points) x = center[0] + rx * np.cos(t) y = center[1] + ry * np.sin(t) slice1 = np.column_stack((x, y)) t0 = get_ellipse_t_for_angle(0, rx, ry) t1 = get_ellipse_t_for_angle(start_angle + span_angle - 2*pi, rx, ry) t = np.linspace(t0, t1, num_points) x = center[0] + rx * np.cos(t) y = center[1] + ry * np.sin(t) slice2 = np.column_stack((x, y)) res = np.concatenate((slice1, slice2)) else: end_angle = start_angle + span_angle t0 = get_ellipse_t_for_angle(start_angle, rx, ry) t1 = get_ellipse_t_for_angle(end_angle, rx, ry) t = np.linspace(t0, t1, num_points) x = center[0] + rx * np.cos(t) y = center[1] + ry * np.sin(t) res = np.column_stack((x, y)) return res
[docs] def ellipse_points(center, a, b, num_points): """Generate points on an ellipse. These are generated from the parametric equations of the ellipse. They are not evenly spaced. Args: center (tuple): (x, y) coordinates of the ellipse center. a (float): Length of the semi-major axis. b (float): Length of the semi-minor axis. num_points (int): Number of points to generate. Returns: numpy.ndarray: Array of (x, y) coordinates of the ellipse points. """ t = np.linspace(0, 2 * pi, num_points) x = center[0] + a * np.cos(t) y = center[1] + b * np.sin(t) return np.column_stack((x, y))
[docs] def elliptic_arclength(t_0, t_1, a, b): '''Return the arclength of an ellipse between the given parametric angles. The ellipse has semi-major axis a and semi-minor axis b. Args: t_0 (float): Starting parametric angle. t_1 (float): Ending parametric angle. a (float): Semi-major axis of the ellipse. b (float): Semi-minor axis of the ellipse. Returns: float: Arclength of the ellipse. ''' m = 1 - (b / a)**2 t1 = ellipeinc(t_1 - 0.5 * pi, m) t0 = ellipeinc(t_0 - 0.5 * pi, m) return a*(t1 - t0)
[docs] def central_to_parametric_angle(a, b, phi): """ Converts a central angle to a parametric angle on an ellipse. Args: a (float): Semi-major axis of the ellipse. b (float): Semi-minor axis of the ellipse. phi (float): Angle of the line intersecting the center and the point. Returns: float: Parametric angle (in radians). """ t = atan2((a/b) * sin(phi), cos(phi)) if t < 0: t += 2 * pi return t
[docs] def parametric_to_central_angle(a, b, t): """ Converts a parametric angle on an ellipse to a central angle. Args: a (float): Semi-major axis of the ellipse. b (float): Semi-minor axis of the ellipse. t (float): Parametric angle (in radians). Returns: float: Angle of the line intersecting the center and the point. """ phi = atan2((b/a) * sin(t), cos(t)) if phi < 0: phi += 2 * pi return phi
[docs] def ellipse_point(a, b, angle): '''Return a point on an ellipse with the given a=width/2, b=height/2, and angle. angle is the central-angle and in radians. Args: a (float): Semi-major axis of the ellipse. b (float): Semi-minor axis of the ellipse. angle (float): Central angle in radians. Returns: tuple: Coordinates of the point on the ellipse. ''' r = r_central(a, b, angle) return (r * cos(angle), r * sin(angle))
[docs] def ellipse_param_point(a, b, t): '''Return a point on an ellipse with the given a=width/2, b=height/2, and parametric angle. t is the parametric angle and in radians. Args: a (float): Semi-major axis of the ellipse. b (float): Semi-minor axis of the ellipse. t (float): Parametric angle in radians. Returns: tuple: Coordinates of the point on the ellipse. ''' return (a * cos(t), b * sin(t))
[docs] def get_ellipse_t_for_angle(angle, a, b): """ Calculates the parameter t for a given angle on an ellipse. Args: angle (float): The angle in radians. a (float): Semi-major axis of the ellipse. b (float): Semi-minor axis of the ellipse. Returns: float: The parameter t. """ t = atan2(a * sin(angle), b * cos(angle)) if t < 0: t += 2 * pi return t
[docs] def ellipse_central_angle(t, a, b): """ Calculates the central angle of an ellipse for a given parameter t. Args: t (float): The parameter value. a (float): The semi-major axis of the ellipse. b (float): The semi-minor axis of the ellipse. Returns: float: The central angle in radians. """ theta = atan2(a * sin(t), b * cos(t)) return theta