# coding=utf-8
"""2D Arc"""
from __future__ import division
import math
from .pointvector import Point2D, Vector2D
from .polyline import Polyline2D
from ..intersection2d import closest_point2d_on_arc2d, intersect_line2d_arc2d, \
intersect_line2d_infinite_arc2d
[docs]
class Arc2D(object):
"""2D arc object.
Args:
c: A Point2D representing the center of the arc.
r: A number representing the radius of the arc.
a1: A number between 0 and 2 * pi for the start angle of the arc.
Note that the direction of the arc is always counterclockwise.
a2: A number between 0 and 2 * pi for the end angle of the arc.
Note that the direction of the arc is always counterclockwise.
Properties:
* c
* r
* a1
* a2
* p1
* p2
* midpoint
* min
* max
* length
* angle
* is_circle
* is_inverted
"""
__slots__ = (
'_c', '_r', '_a1', '_a2', '_cos_a1', '_sin_a1', '_cos_a2', '_sin_a2',
'_min', '_max')
def __init__(self, c, r, a1=0, a2=2 * math.pi):
"""Initialize Arc2D."""
assert isinstance(c, Point2D), "Expected Point2D. Got {}.".format(type(c))
assert r > 0, 'Arc radius must be greater than 0. Got {}.'.format(r)
assert 0 <= a1 <= 2 * math.pi, 'Arc start angle must be between 0 and 2*pi. ' \
'Got {}.'.format(a1)
assert 0 <= a2 <= 2 * math.pi, 'Arc start angle must be between 0 and 2*pi. ' \
'Got {}.'.format(a2)
self._c = c
self._r = r
self._a1 = a1
self._a2 = a2
self._cos_a1 = math.cos(a1)
self._sin_a1 = math.sin(a1)
self._cos_a2 = math.cos(a2)
self._sin_a2 = math.sin(a2)
self._min = None
self._max = None
[docs]
@classmethod
def from_dict(cls, data):
"""Create a Arc2D from a dictionary.
Args:
data: A python dictionary in the following format
.. code-block:: python
{
"type": "Arc2D"
"c": (10, 0),
"r": 5,
"a1": 0,
"a2": 3.14159
}
"""
return cls(Point2D.from_array(data['c']),
data['r'], data['a1'], data['a2'])
[docs]
@classmethod
def from_start_mid_end(cls, p1, m, p2, circle=False):
"""Initialize a new arc from start, middle, and end points.
Note that input points will be assumed to be in counterclockwise order.
Args:
p1: The start point of the arc.
m: Any point along the length of the arc that is not the start or end.
p2: The end point of the arc.
circle: Set to True if you would like the output to be a full circle
defined by the three points instead of an arc with a start and end.
Default is False.
"""
for pt in (p1, m, p2):
assert isinstance(pt, Point2D), "Expected Point2D. Got {}.".format(type(pt))
e1 = (p1.x ** 2 + p1.y ** 2)
e2 = (m.x ** 2 + m.y ** 2)
e3 = (p2.x ** 2 + p2.y ** 2)
den = 2 * (p1.x * (m.y - p2.y) - p1.y * (m.x - p2.x) + m.x * p2.y - p2.x * m.y)
try:
x = -(e1 * (p2.y - m.y) + e2 * (p1.y - p2.y) + e3 * (m.y - p1.y)) / den
y = -(e1 * (m.x - p2.x) + e2 * (p2.x - p1.x) + e3 * (p1.x - m.x)) / den
except ZeroDivisionError:
raise ValueError('Input points {}, {}, {} are colinear and '
'cannot define an arc.'.format(p1, m, p2))
r = math.sqrt((x - p1.x) ** 2 + (y - p1.y) ** 2)
if circle is True:
return cls(Point2D(x, y), r)
else:
a1 = Vector2D(1, 0).angle_counterclockwise(Vector2D(p1.x - x, p1.y - y))
a2 = Vector2D(1, 0).angle_counterclockwise(Vector2D(p2.x - x, p2.y - y))
return cls(Point2D(x, y), r, a1, a2)
@property
def c(self):
"""Center point of the circle on which the arc lies."""
return self._c
@property
def r(self):
"""Radius of arc."""
return self._r
@property
def a1(self):
"""Start angle of the arc in radians."""
return self._a1
@property
def a2(self):
"""End angle of the arc in radians."""
return self._a2
@property
def p1(self):
"""Start point."""
return Point2D(
self.c.x + self._cos_a1 * self.r, self.c.y + self._sin_a1 * self.r)
@property
def p2(self):
"""End point."""
return Point2D(
self.c.x + self._cos_a2 * self.r, self.c.y + self._sin_a2 * self.r)
@property
def midpoint(self):
"""Midpoint."""
return self.point_at(0.5)
@property
def min(self):
"""A Point2D for the minimum bounding rectangle vertex around this geometry."""
if self._min is None:
self._calculate_min_max()
return self._min
@property
def max(self):
"""A Point2D for the maximum bounding rectangle vertex around this geometry."""
if self._max is None:
self._calculate_min_max()
return self._max
@property
def length(self):
"""The length of the arc."""
return self.angle * self.r
@property
def angle(self):
"""The total angle over the domain of the arc in radians."""
_diff = self._a2 - self._a1
return _diff if not self.is_inverted else 2 * math.pi + _diff
@property
def area(self):
"""Area of the circle to which the arc belongs."""
assert self.is_circle, 'Arc must be a closed circle to access "area" property.'
return math.pi * self.r ** 2
@property
def is_circle(self):
"""Boolean for whether the arc is a full circle (True) or not (False)."""
return self.a1 == 0 and self.a2 == 2 * math.pi
@property
def is_inverted(self):
"""Boolean noting whether the end angle a2 is smaller than the start angle a1."""
return self._a2 < self._a1
[docs]
def move(self, moving_vec):
"""Get an arc that has been moved along a vector.
Args:
moving_vec: A Vector2D with the direction and distance to move the arc.
"""
return Arc2D(self.c.move(moving_vec), self.r, self.a1, self.a2)
[docs]
def rotate(self, angle, origin):
"""Get a arc that is rotated counterclockwise by a certain angle.
Args:
angle: An angle for rotation in radians.
origin: A Point2D for the origin around which the arc will
be rotated.
"""
_a1 = self.a1 + angle
_a2 = self.a2 + angle
_a1 = _a1 - 2 * math.pi if _a1 > 2 * math.pi else _a1
_a2 = _a2 - 2 * math.pi if _a2 > 2 * math.pi else _a2
return Arc2D(self.c.rotate(angle, origin), self.r, _a1, _a2)
[docs]
def reflect(self, normal, origin):
"""Get a arc reflected across a plane with the input normal vector and origin.
Args:
normal: A Vector2D representing the normal vector for the plane across
which the arc will be reflected. THIS VECTOR MUST BE NORMALIZED.
origin: A Point2D representing the origin from which to reflect.
"""
return Arc2D.from_start_mid_end(self.p2.reflect(normal, origin),
self.midpoint.reflect(normal, origin),
self.p1.reflect(normal, origin))
[docs]
def scale(self, factor, origin=None):
"""Scale a arc by a factor from an origin point.
Args:
factor: A number representing how much the arc should be scaled.
origin: A Point2D representing the origin from which to scale.
If None, it will be scaled from the World origin (0, 0).
"""
return Arc2D(self.c.scale(factor, origin), self.r * factor, self.a1, self.a2)
[docs]
def subdivide(self, distances):
"""Get Point2D values along the arc that subdivide it based on input distances.
Args:
distances: A list of distances along the arc at which to subdivide it.
This can also be a single number that will be repeated until the
end of the arc.
"""
if isinstance(distances, (float, int)):
distances = [distances]
arc_length = self.length
dist = distances[0]
index = 0
sub_pts = [self.p1]
while dist < arc_length:
sub_pts.append(self.point_at_length(dist))
if index < len(distances) - 1:
index += 1
dist += distances[index]
sub_pts.append(self.p2)
return sub_pts
[docs]
def subdivide_evenly(self, number):
"""Get Point2D values along the arc that divide it into evenly-spaced segments.
Args:
number: The number of segments into which the arc will be divided.
"""
interval = 1 / number
parameter = interval
sub_pts = [self.p1]
while parameter <= 1.000000001:
sub_pts.append(self.point_at(parameter))
parameter += interval
return sub_pts
[docs]
def point_at(self, parameter):
"""Get a point at a given fraction along the arc.
Args:
parameter: The fraction between the start and end point where the
desired point lies. For example, 0.5 will yield the midpoint.
"""
_ang = self._a1 + self.angle * parameter
_ang = _ang if _ang <= math.pi * 2 else _ang - math.pi * 2
return Point2D(
self.c.x + math.cos(_ang) * self.r, self.c.y + math.sin(_ang) * self.r)
[docs]
def point_at_angle(self, angle):
"""Get a point at a given angle along the arc.
Args:
angle: The angle in radians from the start point along the arc
to get the Point2D.
"""
_ang = self._a1 + angle
_ang = _ang if _ang <= math.pi * 2 else _ang - math.pi * 2
return Point2D(
self.c.x + math.cos(_ang) * self.r, self.c.y + math.sin(_ang) * self.r)
[docs]
def point_at_length(self, length):
"""Get a point at a given distance along the arc segment.
Args:
length: The distance along the arc from the start point where the
desired point lies.
"""
return self.point_at(length / self.length)
[docs]
def closest_point(self, point):
"""Get the closest Point2D on this object to another Point2D.
Args:
point: A Point2D object to which the closest point on this object
will be computed.
Returns:
Point2D for the closest point on this line to the input point.
"""
return closest_point2d_on_arc2d(point, self)
[docs]
def distance_to_point(self, point):
"""Get the minimum distance between this object and the input point.
Args:
point: A Point2D object to which the minimum distance will be computed.
Returns:
The distance to the input point.
"""
close_pt = self.closest_point(point)
return point.distance_to_point(close_pt)
[docs]
def intersect_line_ray(self, line_ray):
"""Get the intersection between this Arc2D and another Ray2 or LineSegment2D.
Args:
line_ray: Another LineSegment2D or Ray2D or to intersect.
Returns:
A list of 2 Point2D objects if a full intersection exists.
A list with a single Point2D object if the line is tangent or intersects
only once. None if no intersection exists.
"""
return intersect_line2d_arc2d(line_ray, self)
[docs]
def intersect_line_infinite(self, line_ray):
"""Get the intersection between this Arc2D and an infinitely extending Ray2D.
Args:
line_ray: Another LineSegment2D or Ray2D or to intersect.
Returns:
A list of 2 Point2D objects if a full intersection exists.
A list with a single Point2D object if the line is tangent or intersects
only once. None if no intersection exists.
"""
return intersect_line2d_infinite_arc2d(line_ray, self)
[docs]
def split_line_infinite(self, line_ray):
"""Split this Arc2D in 2-3 using an infinitely extending Ray2D or LineSegment2D.
Args:
line_ray: A LineSegment2D or Ray2D that will be extended infinitely for
intersection.
Returns:
A list with 2 or 3 Arc2D objects if the split was successful.
Will be a list with 1 Arc2D if no intersection exists.
"""
inters = intersect_line2d_infinite_arc2d(line_ray, self)
if inters is None:
return [self]
elif self.is_circle:
if len(inters) != 2:
return [self]
a1 = self._a_from_pt(inters[0])
a2 = self._a_from_pt(inters[1])
return [Arc2D(self.c, self.r, a1, a2), Arc2D(self.c, self.r, a2, a1)]
elif len(inters) == 1:
am = self._a_from_pt(inters[0])
return [Arc2D(self.c, self.r, self.a1, am),
Arc2D(self.c, self.r, am, self.a2)]
elif len(inters) == 2:
am1 = self._a_from_pt(inters[0])
am2 = self._a_from_pt(inters[1])
if self._cc_difference(am1) < self._cc_difference(am2):
return [Arc2D(self.c, self.r, self.a1, am1),
Arc2D(self.c, self.r, am1, am2),
Arc2D(self.c, self.r, am2, self.a2)]
else:
return [Arc2D(self.c, self.r, self.a1, am2),
Arc2D(self.c, self.r, am2, am1),
Arc2D(self.c, self.r, am1, self.a2)]
[docs]
def to_polyline(self, divisions, interpolated=True):
"""Get this Arc2D as an approximated Polyline2D.
Args:
divisions: The number of segments into which the arc will be divided.
interpolated: Boolean to note whether the polyline should be interpolated
between the input vertices when it is translated to other interfaces.
This property has no effect on the geometric calculations performed
by this library and is only present in order to assist with
display/translation. (Default: True)
"""
pts = self.subdivide_evenly(divisions)
return Polyline2D(pts, interpolated)
[docs]
def to_dict(self):
"""Get Arc2D as a dictionary."""
return {'type': 'Arc2D', 'c': self.c.to_array(),
'r': self.r, 'a1': self.a1, 'a2': self.a2}
[docs]
def duplicate(self):
"""Get a copy of this object."""
return self.__copy__()
def _pt_in(self, point):
if self.is_circle:
return True
else:
v = Vector2D(point.x - self.c.x, point.y - self.c.y)
a = Vector2D(1, 0).angle_counterclockwise(v)
return (not self.is_inverted and self.a1 < a < self.a2) or \
(self.is_inverted and self.a1 > a > self.a2)
def _a_from_pt(self, point):
"""Get the angle along the arc given a point along the arc."""
v = Vector2D(point.x - self.c.x, point.y - self.c.y)
return Vector2D(1, 0).angle_counterclockwise(v)
def _cc_difference(self, angle):
"""Get counterclockwise different between an angle and the start of this arc."""
_diff = angle - self.a1
return _diff if not angle < self.a1 else 2 * math.pi + _diff
def _calculate_min_max(self):
"""Calculate maximum and minimum Point2D for this object."""
# get the quadrants of the start and end of the arc
start_quad = self._angle_quadrant(self._a1)
end_quad = self._angle_quadrant(self._a2)
# get the min and max of the start and end points
x_cor = (self._cos_a1 * self.r, self._cos_a2 * self.r)
y_cor = (self._sin_a1 * self.r, self._sin_a2 * self.r)
mnx, mny = min(x_cor), min(y_cor)
mxx, mxy = max(x_cor), max(y_cor)
# build extremum matrices
r = self.r
x_max = ((mxx, r, r, r), (mxx, mxx, r, r),
(mxx, mxx, mxx, r), (mxx, mxx, mxx, mxx))
y_max = ((mxy, mxy, mxy, mxy), (r, mxy, r, r),
(r, mxy, mxy, r), (r, mxy, mxy, mxy))
x_min = ((mnx, -r, mnx, mnx), (mnx, mnx, mnx, mnx),
(-r, -r, mnx, -r), (-r, -r, mnx, mnx))
y_min = ((mny, -r, -r, mny), (mny, mny, -r, mny),
(mny, mny, mny, mny), (-r, -r, -r, mny))
# select the desired values from the extremum matrices
min_pt = (x_min[end_quad][start_quad], y_min[end_quad][start_quad])
max_pt = (x_max[end_quad][start_quad], y_max[end_quad][start_quad])
self._min = Point2D(min_pt[0] + self.c.x, min_pt[1] + self.c.y)
self._max = Point2D(max_pt[0] + self.c.x, max_pt[1] + self.c.y)
@staticmethod
def _angle_quadrant(angle):
"""Get the quadrant of a given angle in radians."""
if angle < math.pi / 2:
return 0
elif angle < math.pi:
return 1
elif angle < math.pi * (3 / 2):
return 2
return 3
def __copy__(self):
return Arc2D(self.c, self.r, self.a1, self.a2)
def __key(self):
"""A tuple based on the object properties, useful for hashing."""
return (self.c, self.r, self.a1, self.a2)
def __hash__(self):
return hash(self.__key())
def __eq__(self, other):
return isinstance(other, Arc2D) and self.__key() == other.__key()
def __ne__(self, other):
return not self.__eq__(other)
[docs]
def ToString(self):
"""Overwrite .NET ToString."""
return self.__repr__()
def __repr__(self):
return 'Arc2D (center <%.2f, %.2f>) (radius <%.2f>) (length <%.2f>)' % \
(self.c.x, self.c.y, self.r, self.length)