From f8a87e2d2e76e058614ff913e1eaa7f019d2ed7f Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 21 Mar 2019 15:38:37 +0100 Subject: [PATCH] Vectorize Arc.draw. ... by replacing generators into functions working on numpy arrays. All modified functions are nested and thus not publically accessible. Also remove handling the tangential case as the angles are immediately stuffed in a set() so duplicate angles don't matter. --- lib/matplotlib/patches.py | 64 +++++++------------ .../test_patches/large_arc.svg | 40 ++++++++++++ lib/matplotlib/tests/test_patches.py | 8 +++ 3 files changed, 70 insertions(+), 42 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_patches/large_arc.svg diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index f133e8c5e7c0..343e7ba44f98 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -1561,14 +1561,12 @@ def draw(self, renderer): calculation much easier than doing rotated ellipse intersection directly). - This uses the "line intersecting a circle" algorithm - from: + This uses the "line intersecting a circle" algorithm from: Vince, John. *Geometry for Computer Graphics: Formulae, Examples & Proofs.* London: Springer-Verlag, 2005. - 2. The angles of each of the intersection points are - calculated. + 2. The angles of each of the intersection points are calculated. 3. Proceeding counterclockwise starting in the positive x-direction, each of the visible arc-segments between the @@ -1601,33 +1599,25 @@ def theta_stretch(theta, scale): self._path = Path.arc(theta1, theta2) return Patch.draw(self, renderer) - def iter_circle_intersect_on_line(x0, y0, x1, y1): + def line_circle_intersect(x0, y0, x1, y1): dx = x1 - x0 dy = y1 - y0 dr2 = dx * dx + dy * dy D = x0 * y1 - x1 * y0 D2 = D * D discrim = dr2 - D2 - - # Single (tangential) intersection - if discrim == 0.0: - x = (D * dy) / dr2 - y = (-D * dx) / dr2 - yield x, y - elif discrim > 0.0: - # The definition of "sign" here is different from - # np.sign: we never want to get 0.0 - if dy < 0.0: - sign_dy = -1.0 - else: - sign_dy = 1.0 + if discrim >= 0.0: + sign_dy = np.copysign(1, dy) # +/-1, never 0. sqrt_discrim = np.sqrt(discrim) - for sign in (1., -1.): - x = (D * dy + sign * sign_dy * dx * sqrt_discrim) / dr2 - y = (-D * dx + sign * np.abs(dy) * sqrt_discrim) / dr2 - yield x, y + return np.array( + [[(D * dy + sign_dy * dx * sqrt_discrim) / dr2, + (-D * dx + abs(dy) * sqrt_discrim) / dr2], + [(D * dy - sign_dy * dx * sqrt_discrim) / dr2, + (-D * dx - abs(dy) * sqrt_discrim) / dr2]]) + else: + return np.empty((0, 2)) - def iter_circle_intersect_on_line_seg(x0, y0, x1, y1): + def segment_circle_intersect(x0, y0, x1, y1): epsilon = 1e-9 if x1 < x0: x0e, x1e = x1, x0 @@ -1637,17 +1627,13 @@ def iter_circle_intersect_on_line_seg(x0, y0, x1, y1): y0e, y1e = y1, y0 else: y0e, y1e = y0, y1 - x0e -= epsilon - y0e -= epsilon - x1e += epsilon - y1e += epsilon - for x, y in iter_circle_intersect_on_line(x0, y0, x1, y1): - if x0e <= x <= x1e and y0e <= y <= y1e: - yield x, y + xys = line_circle_intersect(x0, y0, x1, y1) + xs, ys = xys.T + return xys[(x0e - epsilon < xs) & (xs < x1e + epsilon) + & (y0e - epsilon < ys) & (ys < y1e + epsilon)] # Transforms the axes box_path so that it is relative to the unit - # circle in the same way that it is relative to the desired - # ellipse. + # circle in the same way that it is relative to the desired ellipse. box_path = Path.unit_rectangle() box_path_transform = transforms.BboxTransformTo(self.axes.bbox) + \ self.get_transform().inverted() @@ -1656,16 +1642,10 @@ def iter_circle_intersect_on_line_seg(x0, y0, x1, y1): thetas = set() # For each of the point pairs, there is a line segment for p0, p1 in zip(box_path.vertices[:-1], box_path.vertices[1:]): - x0, y0 = p0 - x1, y1 = p1 - for x, y in iter_circle_intersect_on_line_seg(x0, y0, x1, y1): - theta = np.arccos(x) - if y < 0: - theta = 2 * np.pi - theta - # Convert radians to angles - theta = np.rad2deg(theta) - if theta1 < theta < theta2: - thetas.add(theta) + xy = segment_circle_intersect(*p0, *p1) + x, y = xy.T + theta = np.rad2deg(np.arctan2(y, x)) + thetas.update(theta[(theta1 < theta) & (theta < theta2)]) thetas = sorted(thetas) + [theta2] last_theta = theta1 diff --git a/lib/matplotlib/tests/baseline_images/test_patches/large_arc.svg b/lib/matplotlib/tests/baseline_images/test_patches/large_arc.svg new file mode 100644 index 000000000000..96cd6b203314 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_patches/large_arc.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index e047ea696e97..885f7e51ea19 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -478,3 +478,11 @@ def test_fancyarrow_units(): fig, ax = plt.subplots() arrow = FancyArrowPatch((0, dtime), (0.01, dtime)) ax.add_patch(arrow) + + +@image_comparison(["large_arc.svg"], style="mpl20") +def test_large_arc(): + ax = plt.figure().add_subplot() + ax.set_axis_off() + # A large arc that crosses the axes view limits. + ax.add_patch(mpatches.Arc((-100, 0), 201, 201))