From 0d109400c69de2de364a888e80d8f0e7f9ae93f6 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sun, 27 Oct 2024 13:18:04 +0000 Subject: [PATCH 1/2] Move polygon only when the pointer is on the polygon --- lib/matplotlib/tests/test_widgets.py | 31 +++++++++++++++++++++++++--- lib/matplotlib/widgets.py | 12 ++++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 585d846944e8..dc449cd893d7 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1458,10 +1458,10 @@ def test_polygon_selector(draw_bounding_box): *polygon_place_vertex(50, 150), *polygon_place_vertex(50, 50), ('on_key_press', dict(key='shift')), + ('onmove', dict(xdata=75, ydata=75)), + ('press', dict(xdata=75, ydata=75)), ('onmove', dict(xdata=100, ydata=100)), - ('press', dict(xdata=100, ydata=100)), - ('onmove', dict(xdata=125, ydata=125)), - ('release', dict(xdata=125, ydata=125)), + ('release', dict(xdata=100, ydata=100)), ('on_key_release', dict(key='shift')), ] check_selector(event_sequence, expected_result, 2) @@ -1704,6 +1704,31 @@ def test_polygon_selector_clear_method(ax): np.testing.assert_equal(artist.get_xydata(), [(0, 0)]) +def test_polygon_selector_move(ax): + onselect = mock.Mock(spec=noop, return_value=None) + tool = widgets.PolygonSelector(ax, onselect) + vertices = [(10, 40), (50, 90), (30, 20)] + tool.verts = vertices + + # don't move polygon when pointer is outside of the polygon + press_data = (100, 100) + release_data = (110, 110) + click_and_drag(tool, start=press_data, end=release_data, key="shift") + assert tool.verts == vertices + + # don't move polygon when shift key is not press + press_data = (25, 45) + release_data = (35, 55) + click_and_drag(tool, start=press_data, end=release_data, key=None) + assert tool.verts == vertices + + # move polygon when the pointer is on polygon + press_data = (25, 45) + release_data = (35, 55) + click_and_drag(tool, start=press_data, end=release_data, key="shift") + np.testing.assert_allclose(tool.verts, np.array(vertices) + 10) + + @pytest.mark.parametrize("horizOn", [False, True]) @pytest.mark.parametrize("vertOn", [False, True]) def test_MultiCursor(horizOn, vertOn): diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 9c676574310c..50dd7c42f272 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -3995,7 +3995,12 @@ def _onmove(self, event): self._xys[-1] = self._get_data_coords(event) # Move all vertices. - elif 'move_all' in self._state and self._eventpress: + elif ( + 'move_all' in self._state and self._eventpress and + # Allow `move_all` when not completed + # or if the event is contained in the polygon + (not self._selection_completed or self._contains(event)) + ): xdata, ydata = self._get_data_coords(event) dx = xdata - self._eventpress.xdata dy = ydata - self._eventpress.ydata @@ -4070,6 +4075,11 @@ def _draw_polygon(self): self._draw_polygon_without_update() self.update() + def _contains(self, event): + return mpl.path.Path(self.verts).contains_points( + [self._get_data_coords(event), ] + ) + @property def verts(self): """The polygon vertices, as a list of ``(x, y)`` pairs.""" From f08286617f38e964ff933d33e8eab9152ccd6d1c Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Thu, 31 Oct 2024 19:05:55 +0000 Subject: [PATCH 2/2] Add support for selecting/de-selecting polygon --- lib/matplotlib/tests/test_widgets.py | 8 ++++++-- lib/matplotlib/widgets.py | 25 ++++++++++++++++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index dc449cd893d7..5b960c32aa6e 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1709,23 +1709,27 @@ def test_polygon_selector_move(ax): tool = widgets.PolygonSelector(ax, onselect) vertices = [(10, 40), (50, 90), (30, 20)] tool.verts = vertices + assert tool._selected # don't move polygon when pointer is outside of the polygon press_data = (100, 100) release_data = (110, 110) click_and_drag(tool, start=press_data, end=release_data, key="shift") assert tool.verts == vertices + assert not tool._selected # don't move polygon when shift key is not press press_data = (25, 45) release_data = (35, 55) click_and_drag(tool, start=press_data, end=release_data, key=None) assert tool.verts == vertices + assert tool._selected # move polygon when the pointer is on polygon - press_data = (25, 45) - release_data = (35, 55) + press_data = (25, 35) + release_data = (35, 45) click_and_drag(tool, start=press_data, end=release_data, key="shift") + assert tool._selected np.testing.assert_allclose(tool.verts, np.array(vertices) + 10) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 50dd7c42f272..96b1e66dc791 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2125,6 +2125,9 @@ def __init__(self, ax, onselect=None, useblit=False, button=None, self._eventrelease = None self._prev_event = None self._state = set() + # keep track if selector is selected or not + # Default to True + self._selected = True def set_active(self, active): super().set_active(active) @@ -2352,8 +2355,9 @@ def get_visible(self): def clear(self): """Clear the selection and set the selector ready to make a new one.""" - self._clear_without_update() - self.update() + if self._selected: + self._clear_without_update() + self.update() def _clear_without_update(self): self._selection_completed = False @@ -3937,6 +3941,14 @@ def _press(self, event): h_idx, h_dist = self._polygon_handles.closest(event.x, event.y) if h_dist < self.grab_range: self._active_handle_idx = h_idx + # Only check if contains when polygon is completed + if self._selection_completed: + # contains shape + handle + contains = self._contains(event) or self._active_handle_idx >= 0 + if contains != self._selected: + self._selected = contains + self._polygon_handles.set_visible(self._selected) + self._draw_polygon() # Save the vertex positions at the time of the press event (needed to # support the 'move_all' state modifier). self._xys_at_press = self._xys.copy() @@ -3995,12 +4007,7 @@ def _onmove(self, event): self._xys[-1] = self._get_data_coords(event) # Move all vertices. - elif ( - 'move_all' in self._state and self._eventpress and - # Allow `move_all` when not completed - # or if the event is contained in the polygon - (not self._selection_completed or self._contains(event)) - ): + elif 'move_all' in self._state and self._eventpress and self._selected: xdata, ydata = self._get_data_coords(event) dx = xdata - self._eventpress.xdata dy = ydata - self._eventpress.ydata @@ -4048,7 +4055,7 @@ def _on_key_release(self, event): self._xys.append(self._get_data_coords(event)) self._draw_polygon() # Reset the polygon if the released key is the 'clear' key. - elif event.key == self._state_modifier_keys.get('clear'): + elif self._selected and event.key == self._state_modifier_keys.get('clear'): event = self._clean_event(event) self._xys = [self._get_data_coords(event)] self._selection_completed = False