@@ -839,6 +839,7 @@ def __init__(self, xy, width, height, angle=0.0, **kwargs):
839
839
self ._width , self ._height = width , height
840
840
self ._angle = angle
841
841
self ._recompute_transform ()
842
+ self ._path = Path .unit_circle ()
842
843
843
844
def _recompute_transform (self ):
844
845
self ._patch_transform = transforms .Affine2D () \
@@ -850,7 +851,7 @@ def get_path(self):
850
851
"""
851
852
Return the vertices of the rectangle
852
853
"""
853
- return Path . unit_circle ()
854
+ return self . _path
854
855
855
856
def get_patch_transform (self ):
856
857
return self ._patch_transform
@@ -881,7 +882,6 @@ def _set_angle(self, angle):
881
882
self ._recompute_transform ()
882
883
angle = property (_get_angle , _set_angle )
883
884
884
-
885
885
class Circle (Ellipse ):
886
886
"""
887
887
A circle patch
@@ -908,6 +908,179 @@ def __init__(self, xy, radius=5, **kwargs):
908
908
Ellipse .__init__ (self , xy , radius * 2 , radius * 2 , ** kwargs )
909
909
__init__ .__doc__ = cbook .dedent (__init__ .__doc__ ) % artist .kwdocd
910
910
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
911
1084
912
1085
def bbox_artist (artist , renderer , props = None , fill = True ):
913
1086
"""
0 commit comments