Source code for fibomat.shapes.arc

"""Provides the :class:`Arc` class."""
from __future__ import annotations

from typing import List, Optional

import numpy as np

from fibomat.linalg import Vector, VectorLike, BoundingBox
from fibomat.shapes.shape import Shape
from fibomat.utils import mod_2pi
from fibomat.shapes.arc_spline import ArcSpline, ArcSplineCompatible


[docs]class Arc(Shape, ArcSplineCompatible): # pylint: disable=too-many-public-methods """Circular arc shape. Some formulas take from `here <http://www.lee-mac.com/bulgeconversion.html>`_. TODO: from_points((-1, 0), (0, 1), (-1, 0)) does not work. Should it!? """
[docs] def __init__( # pylint: disable=too-many-arguments self, radius: float, start_angle: float, end_angle: float, sweep_dir: bool, center: Optional[VectorLike] = None, description: Optional[str] = None ): """ Args: radius (float): radius start_angle (float): starting angle (measured from pos. x-axis) end_angle (float): end angle (measured from pos. x-axis) sweep_dir (bool): if True, arc direction is in mathematical positive direction and in math. negative direction if False center (VectorLike, optional): center of completed arc, default to (0, 0) description (str, optional): description .. warning:: `center` is the center of the circle (aka. the completed arc), not centroid! """ super().__init__(description) self._r: float = float(radius) # make sure that angles are in[0, 2pi] self._start_angle: float = mod_2pi(float(start_angle)) self._end_angle: float = mod_2pi(float(end_angle)) self._sweep_dir = bool(sweep_dir) # self._large_arc = bool(large_arc) self._center: Vector = Vector(center) if center is not None else Vector(0, 0)
[docs] @classmethod def from_bulge(cls, start: VectorLike, end: VectorLike, bulge: float): """ Construct a curve from start and end points and bulge value. See `here <http://www.lee-mac.com/bulgeconversion.html>`_ and `there <https://ezdxf.readthedocs.io/en/stable/dxfentities/lwpolyline.html#bulge-value>`_ for details concerning the bulge value. Args: start (VectorLike): start point end (VectorLike): end point bulge (float): bulge value Returns: Arc """ start = Vector(start) end = Vector(end) bulge = float(bulge) theta_2 = 2 * np.arctan(bulge) radius = (start - end).length / 2 / np.sin(theta_2) phi = np.pi / 2 - theta_2 + (end - start).angle_about_x_axis # print(theta_2, r, phi) center = start+Vector(r=radius, phi=phi) # if bulge < 0.: # start_angle = angle_about_xaxis(end - center) # end_angle = angle_about_xaxis(start - center) # else: start_angle = (start - center).angle_about_x_axis end_angle = (end - center).angle_about_x_axis return cls(abs(radius), start_angle, end_angle, bool(bulge >= 0), center)
[docs] @classmethod def from_points(cls, p1: VectorLike, p2: VectorLike, p3: VectorLike): # pylint: disable=invalid-name """Creates an arc connecting `p1` with `p3` via `p2`. Args: p1 (VectorLike): start point p2 (VectorLike): intermediate point p3 (VectorLike): end point Returns: Arc """ p1 = Vector(p1) p2 = Vector(p2) p3 = Vector(p3) bulge = np.tan((np.pi - (p1 - p2).angle_about_x_axis + (p3 - p2).angle_about_x_axis) / 2) # print('bulge', bulge, (np.pi - angle_about_xaxis(p1 - p2) + angle_about_xaxis(p3 - p2)) / 2) return cls.from_bulge(p1, p3, bulge)
[docs] @classmethod def from_points_center(cls, start: VectorLike, end: VectorLike, center: VectorLike, sweep_dir: bool): """Create Arc from start, end, center and sweep_dir Args: start (VectorLike): start point end (VectorLike): end point center (VectorLike): center sweep_dir (bool): sweep_dir Returns: Arc """ start = Vector(start) end = Vector(end) center = Vector(center) start_angle = (start - center).angle_about_x_axis end_angle = (end - center).angle_about_x_axis radius = np.linalg.norm(start - center) return cls(radius=radius, start_angle=start_angle, end_angle=end_angle, sweep_dir=sweep_dir, center=center)
[docs] @classmethod def from_points_center_tangent( cls, start: VectorLike, end: VectorLike, center: VectorLike, *, unit_tangent_start=None, unit_tangent_end=None ): """Create Arc from start, end, center and tangent at start or end. The sweep_dir is calculated automatically. Args: start (VectorLike): start point end (VectorLike): end point center (VectorLike): center unit_tangent_start (VectorLike, optional): unit tangent at start unit_tangent_end (VectorLike, optional): unit tangent at end Returns: Arc Raises: ValueError: Raised of none or both of unit_tangent_start and unit_tangent_end are defined. """ start = Vector(start) end = Vector(end) center = Vector(center) start_angle = (start - center).angle_about_x_axis end_angle = (end - center).angle_about_x_axis radius = np.linalg.norm(start - center) if unit_tangent_start is not None and unit_tangent_end is None: unit_tangent_start = Vector(unit_tangent_start) sweep_matrix = np.ones(shape=(3, 3), dtype=float) sweep_matrix[0, 1:] = np.asarray(start) sweep_matrix[1, 1:] = np.asarray(start + unit_tangent_start) sweep_matrix[2, 1:] = np.asarray(end) elif unit_tangent_start is None and unit_tangent_end is not None: unit_tangent_end = Vector(unit_tangent_end) sweep_matrix = np.ones(shape=(3, 3), dtype=float) sweep_matrix[0, 1:] = np.asarray(start) sweep_matrix[1, 1:] = np.asarray(end) sweep_matrix[2, 1:] = np.asarray(end + unit_tangent_end) elif unit_tangent_start is None and unit_tangent_end is None: raise ValueError('Anyone of unit_tangent_start or unit_tangent_end must be defined.') else: raise ValueError('unit_tangent_start and unit_tangent_end cannot be defined both.') sweep_dir = bool(np.linalg.det(sweep_matrix) > 0) arc = cls(radius=radius, start_angle=start_angle, end_angle=end_angle, sweep_dir=sweep_dir, center=center) if unit_tangent_start is not None: if not np.allclose(arc.unit_tangent_start, unit_tangent_start): raise RuntimeError else: if not np.allclose(arc.unit_tangent_end, unit_tangent_end): raise RuntimeError return arc
[docs] def to_arc_spline(self) -> ArcSpline: is_closed = self.is_closed if abs(self.bulge) > 1: arcs = self.split() vertices = [(*arcs[0].start, arcs[0].bulge), (*arcs[1].start, arcs[1].bulge)] if not is_closed: vertices.append((*arcs[1].end, 0.)) else: vertices = [(*self.start, self.bulge), (*self.end, 0.)] return ArcSpline(vertices, is_closed, self.description)
@property def start(self) -> Vector: """Start point of arc Access: get Returns: Vector """ return Vector(r=self._r, phi=self._start_angle)+self._center @property def end(self) -> Vector: """End point of arc Access: get Returns: Vector """ return Vector(r=self._r, phi=self._end_angle)+self._center @property def start_angle(self) -> float: """Start angle of arc (measured from pos. x. axis). Access: get Returns: float """ return self._start_angle @property def end_angle(self): """End angle of arc (measured from pos. x. axis). Access: get Returns: float """ return self._end_angle @property def sweep_dir(self) -> bool: """If `True`, arc direction is in mathematical positive direction and in math. negative direction if `False` Access: get Returns: bool """ return self._sweep_dir @property def radius(self) -> float: """Arc radius Access: get Returns: float """ return self._r @property def theta(self) -> float: """Absolute value of enclosed angle Access: get Returns: float """ angle = np.abs(self._end_angle - self._start_angle) if (self._sweep_dir and self._start_angle >= self._end_angle) \ or (not self._sweep_dir and self._end_angle >= self._start_angle): angle = 2*np.pi - angle return angle @property def bulge(self) -> float: """Bulge value = tan(theta/4). bulge > 0 arc goes in math. positve direction and for bulge < 0 in math. neg. direction Access: get Returns: float """ if self._sweep_dir: sign = 1 else: sign = -1 return sign * np.tan(self.theta / 4.) @property def center(self) -> Vector: """Center of enclosing circle. Access: get Returns: Vector """ return self._center @property def midpoint(self) -> Vector: """Midpoint of arc. Access: get Returns: Vector """ sign = 1. if self._sweep_dir else -1. return self.start.rotated(sign * self.theta/2, origin=self._center) @property def unit_tangent_start(self) -> Vector: """Unit tangent at start. Access: get Returns: Vector """ sign = 1. if self._sweep_dir else -1. return sign * Vector(-np.sin(self._start_angle), np.cos(self._start_angle)) @property def unit_tangent_end(self) -> Vector: """Unit tangent at end. Access: get Returns: Vector """ sign = 1. if self._sweep_dir else -1. return sign * Vector(-np.sin(self._end_angle), np.cos(self._end_angle))
[docs] def unit_tangent_at(self, angle): """Unit tangent at specific angle. Args: angle (float): 0 < angle < self.theta Returns: Vector """ if angle > self.theta: raise ValueError('angle > self.theta') sign = 1. if self._sweep_dir else -1. phi = self._start_angle + sign * angle return sign * Vector(-np.sin(phi), np.cos(phi))
@property def length(self): """Length of arc. Access: get Returns: float """ return self.radius * self.theta
[docs] def split(self) -> List[Arc]: """Return two arcs if theta > np.pi (hence the abs(bulge) value of new arcs will be smaller than 1). Returns: List[Arc]: if theta > np.pi list contains two elements and one otherwise. """ # pylint: disable=protected-access theta = self.theta if theta > np.pi: arc_1 = self.clone() arc_2 = self.clone() if self._sweep_dir: arc_1._end_angle = mod_2pi(arc_1._start_angle + theta / 2) arc_2._start_angle = arc_1._end_angle else: arc_1._end_angle = mod_2pi(arc_1._start_angle - theta / 2) arc_2._start_angle = arc_1._end_angle return [arc_1, arc_2] return [self.clone()]
[docs] def split_at(self, angle: float) -> List[Arc]: """Split the arc in two halves ([0, angle], [angle, theta]). Args: angle (float): split angle. Returns: List[Arc] Raises: ValueError: Raised if angle < 0 or angle > self.theta """ # pylint: disable=protected-access if angle < 0 or angle > self.theta: raise ValueError('angle < 0 or angle > self.theta') arc_1 = self.clone() arc_2 = self.clone() if self._sweep_dir: arc_1._end_angle = mod_2pi(arc_1._start_angle + angle) arc_2._start_angle = arc_1._end_angle else: arc_1._end_angle = mod_2pi(arc_1._start_angle - angle) arc_2._start_angle = arc_1._end_angle return [arc_1, arc_2]
# def reverse(self) -> None: # """Reverse the arc in-place. # # Returns: # None # """ # self._start_angle, self._end_angle = self._end_angle, self._start_angle # self._sweep_dir = not self._sweep_dir
[docs] def reversed(self) -> Arc: """Return a reversed version of the arc Returns: Arc """ # pylint: disable=protected-access copy = self.clone() copy._start_angle, copy._end_angle = copy._end_angle, copy._start_angle copy._sweep_dir = not copy._sweep_dir return copy
# def clone(self) -> Arc: # return self.__class__(self._r, self._start_angle, self._end_angle, self._sweep_dir, self._center.clone()) # # __copy__ = clone def __repr__(self) -> str: return 'Arc(r={!r}, start_angle={!r}, end_angle={!r}, sweep_dir={!r}, center={!r})'.format( self.radius, self.start_angle, self.end_angle, self.sweep_dir, self.center ) @property def bounding_box(self) -> BoundingBox: # https://stackoverflow.com/questions/1336663/2d-bounding-box-of-a-sector def axis_intersection(angle: float): return self._center+Vector(self._r, 0).rotated(angle) if np.isclose(self.theta, 2*np.pi): r_vec = Vector(self._r, self._r) return BoundingBox(self.center-r_vec, self.center+r_vec) points = [self._center, self.start, self.end] for axis_angle in [0., np.pi/2, np.pi, 3/2 * np.pi, 2*np.pi]: if self._sweep_dir: if self._end_angle >= self._start_angle: if self._end_angle >= axis_angle >= self._start_angle: points.append(axis_intersection(axis_angle)) else: # self._end_angle < self._start_angle if self._end_angle >= axis_angle or self._start_angle <= axis_angle: points.append(axis_intersection(axis_angle)) else: if self._start_angle >= self._end_angle: if self._start_angle >= axis_angle >= self._end_angle: points.append(axis_intersection(axis_angle)) else: # self._start_angle < self._end_angle if self._end_angle <= axis_angle or self._start_angle >= axis_angle: points.append(axis_intersection(axis_angle)) return BoundingBox.from_points(points) @property def is_closed(self) -> bool: return np.isclose(self.theta, 2*np.pi) def _impl_translate(self, trans_vec: VectorLike) -> None: self._center += Vector(trans_vec) def _impl_rotate(self, theta: float) -> None: theta = float(theta) self._start_angle = mod_2pi(self._start_angle + theta) self._end_angle = mod_2pi(self._end_angle + theta) def _impl_scale(self, fac: float) -> None: self._r *= float(fac) def _impl_mirror(self, mirror_axis: VectorLike) -> None: # pylint: disable=protected-access mirrored_arc = self.__class__.from_points_center( self.start.mirrored(mirror_axis), self.end.mirrored(mirror_axis), self._center.mirrored(mirror_axis), not self._sweep_dir ) self._start_angle = mirrored_arc._start_angle self._end_angle = mirrored_arc._end_angle self._sweep_dir = mirrored_arc._sweep_dir self._center = mirrored_arc._center