diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index a4614f214299..0f7478de0c66 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -12,7 +12,7 @@ from matplotlib.cbook import mplDeprecation from matplotlib import docstring, rcParams from .transforms import (Bbox, IdentityTransform, TransformedBbox, - TransformedPath, Transform) + TransformedPatchPath, TransformedPath, Transform) from .path import Path # Note, matplotlib artists use the doc strings for set and get @@ -685,9 +685,7 @@ def set_clip_path(self, path, transform=None): self._clippath = None success = True elif isinstance(path, Patch): - self._clippath = TransformedPath( - path.get_path(), - path.get_transform()) + self._clippath = TransformedPatchPath(path) success = True elif isinstance(path, tuple): path, transform = path @@ -698,6 +696,9 @@ def set_clip_path(self, path, transform=None): elif isinstance(path, Path): self._clippath = TransformedPath(path, transform) success = True + elif isinstance(path, TransformedPatchPath): + self._clippath = path + success = True elif isinstance(path, TransformedPath): self._clippath = path success = True diff --git a/lib/matplotlib/tests/test_transforms.py b/lib/matplotlib/tests/test_transforms.py index 5190ddc873fe..d961741c06ac 100644 --- a/lib/matplotlib/tests/test_transforms.py +++ b/lib/matplotlib/tests/test_transforms.py @@ -10,7 +10,8 @@ import numpy.testing as np_test from numpy.testing import assert_almost_equal, assert_array_equal from numpy.testing import assert_array_almost_equal -from matplotlib.transforms import Affine2D, BlendedGenericTransform, Bbox +from matplotlib.transforms import (Affine2D, BlendedGenericTransform, Bbox, + TransformedPath, TransformedPatchPath) from matplotlib.path import Path from matplotlib.scale import LogScale from matplotlib.testing.decorators import cleanup, image_comparison @@ -576,6 +577,47 @@ def test_invalid_arguments(): assert_raises(RuntimeError, t.transform, [[1, 2, 3]]) +def test_transformed_path(): + points = [(0, 0), (1, 0), (1, 1), (0, 1)] + codes = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] + path = Path(points, codes) + + trans = mtrans.Affine2D() + trans_path = TransformedPath(path, trans) + assert np.allclose(trans_path.get_fully_transformed_path().vertices, + points) + + # Changing the transform should change the result. + r2 = 1 / np.sqrt(2) + trans.rotate(np.pi / 4) + assert np.allclose(trans_path.get_fully_transformed_path().vertices, + [(0, 0), (r2, r2), (0, 2 * r2), (-r2, r2)]) + + # Changing the path does not change the result (it's cached). + path.points = [(0, 0)] * 4 + assert np.allclose(trans_path.get_fully_transformed_path().vertices, + [(0, 0), (r2, r2), (0, 2 * r2), (-r2, r2)]) + + +def test_transformed_patch_path(): + trans = mtrans.Affine2D() + patch = mpatches.Wedge((0, 0), 1, 45, 135, transform=trans) + + tpatch = TransformedPatchPath(patch) + points = tpatch.get_fully_transformed_path().vertices + + # Changing the transform should change the result. + trans.scale(2) + assert np.allclose(tpatch.get_fully_transformed_path().vertices, + points * 2) + + # Changing the path should change the result (and cancel out the scaling + # from the transform). + patch.set_radius(0.5) + assert np.allclose(tpatch.get_fully_transformed_path().vertices, + points) + + if __name__ == '__main__': import nose nose.runmodule(argv=['-s', '--with-doctest'], exit=False) diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 812047910e11..dd56352255b9 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -2715,6 +2715,46 @@ def get_affine(self): return self._transform.get_affine() +class TransformedPatchPath(TransformedPath): + """ + A :class:`TransformedPatchPath` caches a non-affine transformed copy of + the :class:`~matplotlib.path.Patch`. This cached copy is automatically + updated when the non-affine part of the transform or the patch changes. + """ + def __init__(self, patch): + """ + Create a new :class:`TransformedPatchPath` from the given + :class:`~matplotlib.path.Patch`. + """ + TransformNode.__init__(self) + + transform = patch.get_transform() + self._patch = patch + self._transform = transform + self.set_children(transform) + self._path = patch.get_path() + self._transformed_path = None + self._transformed_points = None + + def _revalidate(self): + patch_path = self._patch.get_path() + # Only recompute if the invalidation includes the non_affine part of + # the transform, or the Patch's Path has changed. + if (self._transformed_path is None or self._path != patch_path or + (self._invalid & self.INVALID_NON_AFFINE == + self.INVALID_NON_AFFINE)): + self._path = patch_path + self._transformed_path = \ + self._transform.transform_path_non_affine(patch_path) + self._transformed_points = \ + Path._fast_from_codes_and_verts( + self._transform.transform_non_affine(patch_path.vertices), + None, + {'interpolation_steps': patch_path._interpolation_steps, + 'should_simplify': patch_path.should_simplify}) + self._invalid = 0 + + def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True): ''' Modify the endpoints of a range as needed to avoid singularities.