diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 83b65be400db..a3fe7b65f813 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -753,6 +753,49 @@ def test_wrap_no_wrap(): assert text._get_wrapped_text() == 'non wrapped text' +@pytest.mark.parametrize( + "x, y, rotation", + [(0.0, 1.0, 0), + (1.0, 0.5, 90), + (0.5, 1.0, 180), + (0.0, 0.5, 270)]) +def test_wrap_on_figure_edge(x, y, rotation): + # Regression test for #31537 - wrap collapsed to zero on figure edges. + s = 'This is a very long text that should be wrapped multiple times.' + fig = plt.figure(figsize=(6, 4)) + t = fig.text(x, y, s, wrap=True, rotation=rotation) + fig.canvas.draw() + + # Compare to a nudged-off-the-edge reference that should wrap the same. + nudge = 1e-4 + x_ref = x - nudge if x == 1.0 else x + nudge if x == 0.0 else x + y_ref = y - nudge if y == 1.0 else y + nudge if y == 0.0 else y + fig_ref = plt.figure(figsize=(6, 4)) + t_ref = fig_ref.text(x_ref, y_ref, s, wrap=True, rotation=rotation) + fig_ref.canvas.draw() + + assert t._get_wrapped_text() == t_ref._get_wrapped_text() + + +def test_wrap_on_figure_edge_transform_rotates_text(): + # transform_rotates_text with an axis-aligned transform can make + # get_rotation() float-round to 360.0, breaking the wrap logic. + s = 'This is a very long text that should be wrapped multiple times.' + + fig = plt.figure(figsize=(6, 4)) + transform = mtransforms.Affine2D().rotate_deg(270) + fig.transFigure + t = fig.text(0.0, 0.0, s, transform=transform, wrap=True, + rotation=90, transform_rotates_text=True) + fig.canvas.draw() + + fig_ref = plt.figure(figsize=(6, 4)) + t_ref = fig_ref.text(0.0, 0.0, s, wrap=True, rotation=0) + fig_ref.canvas.draw() + + assert 0 <= t.get_rotation() < 360 + assert t._get_wrapped_text() == t_ref._get_wrapped_text() + + @check_figures_equal() def test_buffer_size(fig_test, fig_ref): # On old versions of the Agg renderer, large non-ascii single-character diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 9c6478f9c7df..4c8b7d5601a4 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -339,8 +339,10 @@ def _char_index_at(self, x): def get_rotation(self): """Return the text angle in degrees in the range [0, 360).""" if self.get_transform_rotates_text(): - return self.get_transform().transform_angles( + angle = self.get_transform().transform_angles( [self._rotation], [self.get_unitless_position()]).item(0) % 360 + # `(tiny_negative) % 360` can float-round to exactly 360.0. + return 0.0 if angle == 360 else angle else: return self._rotation @@ -757,23 +759,29 @@ def _get_dist_to_box(self, rotation, x0, y0, figure_box): Return the distance from the given points to the boundaries of a rotated box, in pixels. """ - if rotation > 270: - quad = rotation - 270 - h1 = (y0 - figure_box.y0) / math.cos(math.radians(quad)) - h2 = (figure_box.x1 - x0) / math.cos(math.radians(90 - quad)) - elif rotation > 180: - quad = rotation - 180 - h1 = (x0 - figure_box.x0) / math.cos(math.radians(quad)) - h2 = (y0 - figure_box.y0) / math.cos(math.radians(90 - quad)) - elif rotation > 90: - quad = rotation - 90 - h1 = (figure_box.y1 - y0) / math.cos(math.radians(quad)) - h2 = (x0 - figure_box.x0) / math.cos(math.radians(90 - quad)) - else: - h1 = (figure_box.x1 - x0) / math.cos(math.radians(rotation)) - h2 = (figure_box.y1 - y0) / math.cos(math.radians(90 - rotation)) + # Branch bounds are inclusive at the cardinals so each cardinal lands + # in a branch where quad=0 — there cos(0)=1 and sin(0)=0 exactly, + # avoiding the float noise that cos(radians(90)) would introduce. + # sin(radians(x)) replaces cos(radians(90 - x)) so the div-by-0 + # propagates to +inf, and fmin discards it (and any 0/0 NaN at corners). + with np.errstate(divide="ignore", invalid="ignore"): + if rotation >= 270: + quad = rotation - 270 + h1 = (y0 - figure_box.y0) / np.cos(np.radians(quad)) + h2 = (figure_box.x1 - x0) / np.sin(np.radians(quad)) + elif rotation >= 180: + quad = rotation - 180 + h1 = (x0 - figure_box.x0) / np.cos(np.radians(quad)) + h2 = (y0 - figure_box.y0) / np.sin(np.radians(quad)) + elif rotation >= 90: + quad = rotation - 90 + h1 = (figure_box.y1 - y0) / np.cos(np.radians(quad)) + h2 = (x0 - figure_box.x0) / np.sin(np.radians(quad)) + else: + h1 = (figure_box.x1 - x0) / np.cos(np.radians(rotation)) + h2 = (figure_box.y1 - y0) / np.sin(np.radians(rotation)) - return min(h1, h2) + return np.fmin(h1, h2) def _get_rendered_text_width(self, text): """