@@ -1592,14 +1592,12 @@ def draw(self, renderer):
1592
1592
calculation much easier than doing rotated ellipse
1593
1593
intersection directly).
1594
1594
1595
- This uses the "line intersecting a circle" algorithm
1596
- from:
1595
+ This uses the "line intersecting a circle" algorithm from:
1597
1596
1598
1597
Vince, John. *Geometry for Computer Graphics: Formulae,
1599
1598
Examples & Proofs.* London: Springer-Verlag, 2005.
1600
1599
1601
- 2. The angles of each of the intersection points are
1602
- calculated.
1600
+ 2. The angles of each of the intersection points are calculated.
1603
1601
1604
1602
3. Proceeding counterclockwise starting in the positive
1605
1603
x-direction, each of the visible arc-segments between the
@@ -1609,6 +1607,8 @@ def draw(self, renderer):
1609
1607
"""
1610
1608
if not hasattr (self , 'axes' ):
1611
1609
raise RuntimeError ('Arcs can only be used in Axes instances' )
1610
+ if not self .get_visible ():
1611
+ return
1612
1612
1613
1613
self ._recompute_transform ()
1614
1614
@@ -1621,44 +1621,62 @@ def theta_stretch(theta, scale):
1621
1621
theta = np .deg2rad (theta )
1622
1622
x = np .cos (theta )
1623
1623
y = np .sin (theta )
1624
- return np .rad2deg (np .arctan2 (scale * y , x ))
1625
- theta1 = theta_stretch (self .theta1 , width / height )
1626
- theta2 = theta_stretch (self .theta2 , width / height )
1627
-
1628
- # Get width and height in pixels
1629
- width , height = self .get_transform ().transform ((width , height ))
1624
+ stheta = np .rad2deg (np .arctan2 (scale * y , x ))
1625
+ # arctan2 has the range [-pi, pi], we expect [0, 2*pi]
1626
+ return (stheta + 360 ) % 360
1627
+
1628
+ theta1 = self .theta1
1629
+ theta2 = self .theta2
1630
+
1631
+ if (
1632
+ # if we need to stretch the angles because we are distorted
1633
+ width != height
1634
+ # and we are not doing a full circle.
1635
+ #
1636
+ # 0 and 360 do not exactly round-trip through the angle
1637
+ # stretching (due to both float precision limitations and
1638
+ # the difference between the range of arctan2 [-pi, pi] and
1639
+ # this method [0, 360]) so avoid doing it if we don't have to.
1640
+ and not (theta1 != theta2 and theta1 % 360 == theta2 % 360 )
1641
+ ):
1642
+ theta1 = theta_stretch (self .theta1 , width / height )
1643
+ theta2 = theta_stretch (self .theta2 , width / height )
1644
+
1645
+ # Get width and height in pixels we need to use
1646
+ # `self.get_data_transform` rather than `self.get_transform`
1647
+ # because we want the transform from dataspace to the
1648
+ # screen space to estimate how big the arc will be in physical
1649
+ # units when rendered (the transform that we get via
1650
+ # `self.get_transform()` goes from an idealized unit-radius
1651
+ # space to screen space).
1652
+ data_to_screen_trans = self .get_data_transform ()
1653
+ pwidth , pheight = (data_to_screen_trans .transform ((width , height )) -
1654
+ data_to_screen_trans .transform ((0 , 0 )))
1630
1655
inv_error = (1.0 / 1.89818e-6 ) * 0.5
1631
- if width < inv_error and height < inv_error :
1656
+
1657
+ if pwidth < inv_error and pheight < inv_error :
1632
1658
self ._path = Path .arc (theta1 , theta2 )
1633
1659
return Patch .draw (self , renderer )
1634
1660
1635
- def iter_circle_intersect_on_line (x0 , y0 , x1 , y1 ):
1661
+ def line_circle_intersect (x0 , y0 , x1 , y1 ):
1636
1662
dx = x1 - x0
1637
1663
dy = y1 - y0
1638
1664
dr2 = dx * dx + dy * dy
1639
1665
D = x0 * y1 - x1 * y0
1640
1666
D2 = D * D
1641
1667
discrim = dr2 - D2
1642
-
1643
- # Single (tangential) intersection
1644
- if discrim == 0.0 :
1645
- x = (D * dy ) / dr2
1646
- y = (- D * dx ) / dr2
1647
- yield x , y
1648
- elif discrim > 0.0 :
1649
- # The definition of "sign" here is different from
1650
- # np.sign: we never want to get 0.0
1651
- if dy < 0.0 :
1652
- sign_dy = - 1.0
1653
- else :
1654
- sign_dy = 1.0
1668
+ if discrim >= 0.0 :
1669
+ sign_dy = np .copysign (1 , dy ) # +/-1, never 0.
1655
1670
sqrt_discrim = np .sqrt (discrim )
1656
- for sign in (1. , - 1. ):
1657
- x = (D * dy + sign * sign_dy * dx * sqrt_discrim ) / dr2
1658
- y = (- D * dx + sign * np .abs (dy ) * sqrt_discrim ) / dr2
1659
- yield x , y
1671
+ return np .array (
1672
+ [[(D * dy + sign_dy * dx * sqrt_discrim ) / dr2 ,
1673
+ (- D * dx + abs (dy ) * sqrt_discrim ) / dr2 ],
1674
+ [(D * dy - sign_dy * dx * sqrt_discrim ) / dr2 ,
1675
+ (- D * dx - abs (dy ) * sqrt_discrim ) / dr2 ]])
1676
+ else :
1677
+ return np .empty ((0 , 2 ))
1660
1678
1661
- def iter_circle_intersect_on_line_seg (x0 , y0 , x1 , y1 ):
1679
+ def segment_circle_intersect (x0 , y0 , x1 , y1 ):
1662
1680
epsilon = 1e-9
1663
1681
if x1 < x0 :
1664
1682
x0e , x1e = x1 , x0
@@ -1668,40 +1686,34 @@ def iter_circle_intersect_on_line_seg(x0, y0, x1, y1):
1668
1686
y0e , y1e = y1 , y0
1669
1687
else :
1670
1688
y0e , y1e = y0 , y1
1671
- x0e -= epsilon
1672
- y0e -= epsilon
1673
- x1e += epsilon
1674
- y1e += epsilon
1675
- for x , y in iter_circle_intersect_on_line (x0 , y0 , x1 , y1 ):
1676
- if x0e <= x <= x1e and y0e <= y <= y1e :
1677
- yield x , y
1689
+ xys = line_circle_intersect (x0 , y0 , x1 , y1 )
1690
+ xs , ys = xys .T
1691
+ return xys [
1692
+ (x0e - epsilon < xs ) & (xs < x1e + epsilon )
1693
+ & (y0e - epsilon < ys ) & (ys < y1e + epsilon )
1694
+ ]
1678
1695
1679
1696
# Transforms the axes box_path so that it is relative to the unit
1680
1697
# circle in the same way that it is relative to the desired ellipse.
1681
- box_path = Path .unit_rectangle ()
1682
1698
box_path_transform = (transforms .BboxTransformTo (self .axes .bbox )
1683
- - self .get_transform ())
1684
- box_path = box_path .transformed (box_path_transform )
1699
+ + self .get_transform (). inverted ())
1700
+ box_path = Path . unit_rectangle () .transformed (box_path_transform )
1685
1701
1686
1702
thetas = set ()
1687
1703
# For each of the point pairs, there is a line segment
1688
1704
for p0 , p1 in zip (box_path .vertices [:- 1 ], box_path .vertices [1 :]):
1689
- x0 , y0 = p0
1690
- x1 , y1 = p1
1691
- for x , y in iter_circle_intersect_on_line_seg (x0 , y0 , x1 , y1 ):
1692
- theta = np .arccos (x )
1693
- if y < 0 :
1694
- theta = 2 * np .pi - theta
1695
- # Convert radians to angles
1696
- theta = np .rad2deg (theta )
1697
- if theta1 < theta < theta2 :
1698
- thetas .add (theta )
1705
+ xy = segment_circle_intersect (* p0 , * p1 )
1706
+ x , y = xy .T
1707
+ # arctan2 return [-pi, pi), the rest of our angles are in
1708
+ # [0, 360], adjust as needed.
1709
+ theta = (np .rad2deg (np .arctan2 (y , x )) + 360 ) % 360
1710
+ thetas .update (theta [(theta1 < theta ) & (theta < theta2 )])
1699
1711
thetas = sorted (thetas ) + [theta2 ]
1700
-
1701
1712
last_theta = theta1
1702
1713
theta1_rad = np .deg2rad (theta1 )
1703
- inside = box_path .contains_point ((np .cos (theta1_rad ),
1704
- np .sin (theta1_rad )))
1714
+ inside = box_path .contains_point (
1715
+ (np .cos (theta1_rad ), np .sin (theta1_rad ))
1716
+ )
1705
1717
1706
1718
# save original path
1707
1719
path_original = self ._path
0 commit comments