@@ -839,6 +839,7 @@ def __init__(self, xy, width, height, angle=0.0, **kwargs):
839839 self ._width , self ._height = width , height
840840 self ._angle = angle
841841 self ._recompute_transform ()
842+ self ._path = Path .unit_circle ()
842843
843844 def _recompute_transform (self ):
844845 self ._patch_transform = transforms .Affine2D () \
@@ -850,7 +851,7 @@ def get_path(self):
850851 """
851852 Return the vertices of the rectangle
852853 """
853- return Path . unit_circle ()
854+ return self . _path
854855
855856 def get_patch_transform (self ):
856857 return self ._patch_transform
@@ -881,7 +882,6 @@ def _set_angle(self, angle):
881882 self ._recompute_transform ()
882883 angle = property (_get_angle , _set_angle )
883884
884-
885885class Circle (Ellipse ):
886886 """
887887 A circle patch
@@ -908,6 +908,179 @@ def __init__(self, xy, radius=5, **kwargs):
908908 Ellipse .__init__ (self , xy , radius * 2 , radius * 2 , ** kwargs )
909909 __init__ .__doc__ = cbook .dedent (__init__ .__doc__ ) % artist .kwdocd
910910
911+ class Arc (Ellipse ):
912+ """
913+ An elliptical arc. Because it performs various optimizations, it may not be
914+ filled.
915+ """
916+ def __str__ (self ):
917+ return "Arc(%d,%d;%dx%d)" % (self .center [0 ],self .center [1 ],self .width ,self .height )
918+
919+ def __init__ (self , xy , width , height , angle = 0.0 , theta1 = 0.0 , theta2 = 360.0 , ** kwargs ):
920+ """
921+ xy - center of ellipse
922+ width - length of horizontal axis
923+ height - length of vertical axis
924+ angle - rotation in degrees (anti-clockwise)
925+ theta1 - starting angle of the arc in degrees
926+ theta2 - ending angle of the arc in degrees
927+
928+ If theta1 and theta2 are not provided, the arc will form a
929+ complete ellipse.
930+
931+ Valid kwargs are:
932+ %(Patch)s
933+ """
934+ fill = kwargs .pop ('fill' )
935+ if fill :
936+ raise ValueError ("Arc objects can not be filled" )
937+ kwargs ['fill' ] = False
938+
939+ Ellipse .__init__ (self , xy , width , height , angle , ** kwargs )
940+
941+ self ._theta1 = theta1
942+ self ._theta2 = theta2
943+
944+ def draw (self , renderer ):
945+ """
946+ Ellipses are normally drawn using an approximation that uses
947+ eight cubic bezier splines. The error of this approximation
948+ is 1.89818e-6, according to this unverified source:
949+
950+ Lancaster, Don. Approximating a Circle or an Ellipse Using
951+ Four Bezier Cubic Splines.
952+
953+ http://www.tinaja.com/glib/ellipse4.pdf
954+
955+ There is a use case where very large ellipses must be drawn
956+ with very high accuracy, and it is too expensive to render the
957+ entire ellipse with enough segments (either splines or line
958+ segments). Therefore, in the case where either radius of the
959+ ellipse is large enough that the error of the spline
960+ approximation will be visible (greater than one pixel offset
961+ from the ideal), a different technique is used.
962+
963+ In that case, only the visible parts of the ellipse are drawn,
964+ with each visible arc using a fixed number of spline segments
965+ (8). The algorithm proceeds as follows:
966+
967+ 1. The points where the ellipse intersects the axes bounding
968+ box are located. (This is done be performing an inverse
969+ transformation on the axes bbox such that it is relative to
970+ the unit circle -- this makes the intersection calculation
971+ much easier than doing rotated ellipse intersection
972+ directly).
973+
974+ This uses the "line intersecting a circle" algorithm from:
975+
976+ Vince, John. Geometry for Computer Graphics: Formulae,
977+ Examples & Proofs. London: Springer-Verlag, 2005.
978+
979+ 2. The angles of each of the intersection points are
980+ calculated.
981+
982+ 3. Proceeding counterclockwise starting in the positive
983+ x-direction, each of the visible arc-segments between the
984+ pairs of vertices are drawn using the bezier arc
985+ approximation technique implemented in Path.arc().
986+ """
987+ # Get the width and height in pixels
988+ width , height = self .get_transform ().transform_point (
989+ (self ._width , self ._height ))
990+ inv_error = (1.0 / 1.89818e-6 )
991+
992+ if width < inv_error and height < inv_error and False :
993+ self ._path = Path .arc (self ._theta1 , self ._theta2 )
994+ return Patch .draw (self , renderer )
995+
996+ # Transforms the axes box_path so that it is relative to the unit
997+ # circle in the same way that it is relative to the desired
998+ # ellipse.
999+ box_path = Path .unit_rectangle ()
1000+ box_path_transform = transforms .BboxTransformTo (self .axes .bbox ) + \
1001+ self .get_transform ().inverted ()
1002+ box_path = box_path .transformed (box_path_transform )
1003+ vertices = []
1004+
1005+ def iter_circle_intersect_on_line (x0 , y0 , x1 , y1 ):
1006+ dx = x1 - x0
1007+ dy = y1 - y0
1008+ dr2 = dx * dx + dy * dy
1009+ dr = npy .sqrt (dr2 )
1010+ D = x0 * y1 - x1 * y0
1011+ D2 = D * D
1012+ discrim = dr2 - D2
1013+
1014+ # Single (tangential) intersection
1015+ if discrim == 0.0 :
1016+ x = (D * dy ) / dr2
1017+ y = (- D * dx ) / dr2
1018+ yield x , y
1019+ elif discrim > 0.0 :
1020+ if dy < 0 :
1021+ sign_dy = - 1.0
1022+ else :
1023+ sign_dy = 1.0
1024+ sqrt_discrim = npy .sqrt (discrim )
1025+ for sign in (1. , - 1. ):
1026+ x = (D * dy + sign * sign_dy * dx * sqrt_discrim ) / dr2
1027+ y = (- D * dx + sign * npy .abs (dy ) * sqrt_discrim ) / dr2
1028+ yield x , y
1029+
1030+ def iter_circle_intersect_on_line_seg (x0 , y0 , x1 , y1 ):
1031+ epsilon = 1e-9
1032+ if x1 < x0 :
1033+ x0e , x1e = x1 , x0
1034+ else :
1035+ x0e , x1e = x0 , x1
1036+ if y1 < y0 :
1037+ y0e , y1e = y1 , y0
1038+ else :
1039+ y0e , y1e = y0 , y1
1040+ x0e -= epsilon
1041+ y0e -= epsilon
1042+ x1e += epsilon
1043+ y1e += epsilon
1044+ for x , y in iter_circle_intersect_on_line (x0 , y0 , x1 , y1 ):
1045+ if x >= x0e and x <= x1e and y >= y0e and y <= y1e :
1046+ yield x , y
1047+
1048+ PI = npy .pi
1049+ TWOPI = PI * 2.0
1050+ RAD2DEG = 180.0 / PI
1051+ DEG2RAD = PI / 180.0
1052+ theta1 = self ._theta1
1053+ theta2 = self ._theta2
1054+ thetas = {}
1055+ # For each of the point pairs, there is a line segment
1056+ for p0 , p1 in zip (box_path .vertices [:- 1 ], box_path .vertices [1 :]):
1057+ x0 , y0 = p0
1058+ x1 , y1 = p1
1059+ for x , y in iter_circle_intersect_on_line_seg (x0 , y0 , x1 , y1 ):
1060+ # Convert radians to angles
1061+ theta = npy .arccos (x )
1062+ if y < 0 :
1063+ theta = TWOPI - theta
1064+ theta *= RAD2DEG
1065+ if theta > theta1 and theta < theta2 :
1066+ thetas [theta ] = None
1067+
1068+ thetas = thetas .keys ()
1069+ thetas .sort ()
1070+ thetas .append (theta2 )
1071+
1072+ last_theta = theta1
1073+ theta1_rad = theta1 * DEG2RAD
1074+ inside = box_path .contains_point ((npy .cos (theta1_rad ), npy .sin (theta1_rad )))
1075+
1076+ for theta in thetas :
1077+ if inside :
1078+ self ._path = Path .arc (last_theta , theta , 8 )
1079+ Patch .draw (self , renderer )
1080+ inside = False
1081+ else :
1082+ inside = True
1083+ last_theta = theta
9111084
9121085def bbox_artist (artist , renderer , props = None , fill = True ):
9131086 """
0 commit comments