Source code for simetri.geometry.ellipse

'''Functions for working with ellipses.'''
from copy import deepcopy
from math import cos, sin, pi, atan2, sqrt, degrees, ceil

import numpy as np

from ..graphics.shape import Shape, custom_attributes
from ..graphics.batch import Batch
from ..graphics.points import Points
from ..graphics.affine import rotation_matrix
from ..graphics.common import Point
from ..graphics.all_enums import Types
from ..geometry.geometry import line_angle, distance, positive_angle, homogenize
from ..canvas.style_map import shape_style_map
from ..settings.settings import defaults
from ..helpers.utilities  import decompose_transformations


[docs] class Arc(Shape): """A circular or elliptic arc defined by a center, radius_x, radius_y, start angle, and span angle. If radius_y is not provided, the arc is a circular arc.""" def __init__( self, center: Point, radius_x: float, radius_y: float = None, start_angle: float = 0, span_angle: float = pi/2, rot_angle: float = 0, n_points: int = None, xform_matrix: 'ndarray' = None, **kwargs, ): """ Args: center (Point): The center of the arc. radius_x (float): The x radius of the arc. radius_y (float): The y radius for elliptical arcs. start_angle (float): The starting angle of the arc. span_angle (float): The span angle of the arc. rot_angle (float, optional): Rotation angle. Defaults to 0. If negative, the arc is drawn clockwise. xform_matrix (ndarray, optional): Transformation matrix. Defaults to None. **kwargs: Additional keyword arguments. """ if radius_y is None: radius_y = radius_x if n_points is None: n = defaults['n_arc_points'] n_points = ceil(n * abs(span_angle) / (2 * pi)) vertices = elliptic_arc_points(center, radius_x, radius_y, start_angle, span_angle, n_points=n_points) if rot_angle: rot_matrix = rotation_matrix(rot_angle, center) if xform_matrix is not None: xform_matrix = np.dot(rot_matrix, xform_matrix) else: xform_matrix = rot_matrix super().__init__(vertices, xform_matrix=xform_matrix, **kwargs) self.subtype = Types.ARC self.n_points = n_points self.__dict__['start_angle'] = start_angle self.__dict__['span_angle'] = span_angle cx, cy = center[:2] self._c = [cx, cy, 1] _a = [radius_x, 0, 1] _b = [0, radius_y, 1] self._orig_triangle = [self._c[:], _a, _b] def __setattr__(self, name, value): """Set an attribute of the arc. Args: name (str): The name of the attribute. value (Any): The value of the attribute. """ if name == "center": diff = np.array(value[:2]) - np.array(self.center[:2]) self.translate(diff[0], diff[1], reps=0) elif name == "radius_x": c, a, _ = self._orig_triangle @ self.xform_matrix cur_radius = distance(c, a) ratio = value / cur_radius self.scale(ratio, 1, about=self.center) elif name == "radius_y": c, _, b = self._orig_triangle @ self.xform_matrix cur_radius = distance(c, b) ratio = value / cur_radius self.scale(1, ratio, about=self.center) elif name == "start_angle": center, a, b = self._orig_triangle @ self.xform_matrix a = distance(center, a) b = distance(center, b) span = self.span_angle n_points = self.n_points points = elliptic_arc_points(center, a, b, value, span, n_points) self.primary_points = Points(points) self.__dict__['start_angle'] = value elif name == "span_angle": center, a, b = self._orig_triangle @ self.xform_matrix a = distance(center, a) b = distance(center, b) start = self.start_angle n_points = self.n_points points = elliptic_arc_points(center, a, b, start, value, n_points) self.primary_points = Points(points) self.__dict__['span_angle'] = value else: super().__setattr__(name, value) @property def center(self): """Return the center of the arc. Returns: Point: The center of the arc. """ return (self._c @ self.xform_matrix).tolist()[:2] @property def radius_x(self): """Return the x radius of the arc. Returns: float: The x radius of the arc. """ c, a, _ = self._orig_triangle @ self.xform_matrix return distance(a, c) @property def radius_y(self): """Return the y radius of the arc. Returns: float: The y radius of the arc. """ c, _, b = self._orig_triangle @ self.xform_matrix return distance(b, c)
[docs] def copy(self): '''Return a copy of the arc.''' center = self.center start_angle = self.start_angle span_angle = self.span_angle radius_x = self.radius_x radius_y = self.radius_y arc = Arc(center, radius_x, radius_y, start_angle, span_angle, rot_angle=0) arc.primary_points = self.primary_points.copy() arc.xform_matrix = self.xform_matrix.copy() arc._orig_triangle = deepcopy(self._orig_triangle) arc._c = self._c[:] arc.n_points = self.n_points for attrib in shape_style_map: setattr(arc, attrib, getattr(self, attrib)) arc.subtype = self.subtype custom_attribs = custom_attributes(self) arc_attribs = ['center', 'start_angle', 'span_angle', 'radius_x', 'radius_y'] for attrib in custom_attribs: if attrib not in arc_attribs: setattr(arc, attrib, getattr(self, attrib)) return arc
[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 def _update(self, xform_matrix: np.array, reps: int = 0) -> Batch: """Used internally. Update the shape with a transformation matrix. Args: xform_matrix (array): The transformation matrix. reps (int, optional): The number of repetitions, defaults to 0. Returns: Batch: The updated shape or a batch of shapes. """ if reps == 0: center = list(self.center[:2]) + [1] start = list(self.vertices[0][:2]) + [1] end = list(self.vertices[-1][:2]) + [1] points = [center, start, end] center2, start2, end2 = np.dot(points, xform_matrix).tolist() self.center = center2[:2] self.start_point = start2[:2] self.end_point = end2[:2] self.start_angle = line_angle(center2, start2) return super()._update(xform_matrix, reps)
[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) 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, radius_x, radius_y, start_angle, span_angle, n_points=None): """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. radius_x (float): Length of the semi-major axis. radius_y (float): Length of the semi-minor axis. start_angle (float): Starting angle of the arc. span_angle (float): Span angle of the arc. n_points (int): Number of points to generate. Returns: numpy.ndarray: Array of (x, y) coordinates of the ellipse points. """ rx = radius_x if radius_y is None: radius_y = radius_x ry = radius_y if n_points is None: n = defaults['n_arc_points'] n_points = ceil(n * abs(span_angle) / (2 * pi)) 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, n_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, n_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, n_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, n_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, n_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, n_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, n_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. n_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, n_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. ''' from scipy.special import ellipeinc # this takes too long to import 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