From 4b50a8f7f8935b399b0dc3fd7fc944d1cdc5abd1 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 14 Aug 2021 12:24:42 +0100 Subject: [PATCH 1/5] Fix name coordinate handle: N and S were swapped --- lib/matplotlib/widgets.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 1fcc52198c99..c63ee651869c 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2846,13 +2846,13 @@ def __init__(self, ax, onselect, drawtype='box', 'edgecolor', 'black'), **cbook.normalize_kwargs(handle_props, Line2D)} - self._corner_order = ['NW', 'NE', 'SE', 'SW'] + self._corner_order = ['SW', 'SE', 'NE', 'NW'] xc, yc = self.corners self._corner_handles = ToolHandles(self.ax, xc, yc, marker_props=self._handle_props, useblit=self.useblit) - self._edge_order = ['W', 'N', 'E', 'S'] + self._edge_order = ['W', 'S', 'E', 'N'] xe, ye = self.edge_centers self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s', marker_props=self._handle_props, @@ -2892,6 +2892,7 @@ def _press(self, event): # button, ... if self._interactive and self._selection_artist.get_visible(): self._set_active_handle(event) + self._extents_on_press = self.extents else: self._active_handle = None @@ -2957,22 +2958,31 @@ def _release(self, event): self.update() self._active_handle = None + self._extents_on_press = None return False def _onmove(self, event): """Motion notify event handler.""" + + # resize an existing shape if self._active_handle and self._active_handle != 'C': x0, x1, y0, y1 = self._extents_on_press + # Switch variables so that only x1 and/or y1 are updated on move. + if self._active_handle in ['W', 'SW', 'NW']: + x0, x1 = x1, event.xdata + if self._active_handle in ['S', 'SW', 'SE']: + y0, y1 = y1, event.ydata + if self._active_handle in ['E', 'W'] + self._corner_order: x1 = event.xdata if self._active_handle in ['N', 'S'] + self._corner_order: y1 = event.ydata # move existing shape - elif (('move' in self._state or self._active_handle == 'C' or - (self.drag_from_anywhere and self._contains(event))) and + elif (self._active_handle == 'C' or + (self.drag_from_anywhere and self._contains(event)) and self._extents_on_press is not None): x0, x1, y0, y1 = self._extents_on_press dx = event.xdata - self._eventpress.xdata @@ -3110,7 +3120,6 @@ def _set_active_handle(self, event): if 'move' in self._state: self._active_handle = 'C' - self._extents_on_press = self.extents # Set active handle as closest handle, if mouse click is close enough. elif m_dist < self.grab_range * 2: # Prioritise center handle over other handles @@ -3121,7 +3130,6 @@ def _set_active_handle(self, event): if self.drag_from_anywhere and self._contains(event): # Check if we've clicked inside the region self._active_handle = 'C' - self._extents_on_press = self.extents else: self._active_handle = None return @@ -3132,15 +3140,6 @@ def _set_active_handle(self, event): # Closest to an edge handle self._active_handle = self._edge_order[e_idx] - # Save coordinates of rectangle at the start of handle movement. - x0, x1, y0, y1 = self.extents - # Switch variables so that only x1 and/or y1 are updated on move. - if self._active_handle in ['W', 'SW', 'NW']: - x0, x1 = x1, event.xdata - if self._active_handle in ['N', 'NW', 'NE']: - y0, y1 = y1, event.ydata - self._extents_on_press = x0, x1, y0, y1 - def _contains(self, event): """Return True if event is within the patch.""" return self._selection_artist.contains(event, radius=0)[0] From bad96e522219e5cb1951904bea991f4b4db0fca0 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 14 Aug 2021 14:40:13 +0100 Subject: [PATCH 2/5] Fix center modifier in interactive rectangle selector - in _onmove of existing shape --- lib/matplotlib/tests/test_widgets.py | 152 +++++++++++++++++++++++++++ lib/matplotlib/widgets.py | 53 +++++++--- 2 files changed, 193 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 0e4a7e7daa68..0f604524fb72 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -118,6 +118,158 @@ def onselect(epress, erelease): assert artist.get_alpha() == 0.3 +def test_rectangle_resize(): + ax = get_ax() + + def onselect(epress, erelease): + pass + + tool = widgets.RectangleSelector(ax, onselect, interactive=True) + # Create rectangle + do_event(tool, 'press', xdata=0, ydata=10, button=1) + do_event(tool, 'onmove', xdata=100, ydata=120, button=1) + do_event(tool, 'release', xdata=100, ydata=120, button=1) + assert tool.extents == (0.0, 100.0, 10.0, 120.0) + + # resize NE handle + extents = tool.extents + xdata, ydata = extents[1], extents[3] + xdata_new, ydata_new = xdata + 10, ydata + 5 + do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) + do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) + do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) + assert tool.extents == (0.0, xdata_new, 10.0, ydata_new) + + # resize E handle + extents = tool.extents + xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 + xdata_new, ydata_new = xdata + 10, ydata + do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) + do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) + do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) + assert tool.extents == (0.0, xdata_new, 10.0, 125.0) + + # resize W handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 + xdata_new, ydata_new = xdata + 15, ydata + do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) + do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) + do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) + assert tool.extents == (xdata_new, 120.0, 10.0, 125.0) + + # resize SW handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + xdata_new, ydata_new = xdata + 20, ydata + 25 + do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) + do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) + do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) + assert tool.extents == (xdata_new, 120.0, ydata_new, 125.0) + + +@pytest.mark.parametrize('use_default_state', [True, False]) +def test_rectangle_resize_center(use_default_state): + ax = get_ax() + + def onselect(epress, erelease): + pass + + tool = widgets.RectangleSelector(ax, onselect, interactive=True) + # Create rectangle + do_event(tool, 'press', xdata=70, ydata=65, button=1) + do_event(tool, 'onmove', xdata=125, ydata=130, button=1) + do_event(tool, 'release', xdata=125, ydata=130, button=1) + assert tool.extents == (70.0, 125.0, 65.0, 130.0) + + if use_default_state: + tool._default_state.add('center') + + # resize NE handle + extents = tool.extents + xdata, ydata = extents[1], extents[3] + xdiff, ydiff = 10, 5 + xdata_new, ydata_new = xdata + xdiff, ydata + ydiff + do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) + if not use_default_state: + do_event(tool, 'on_key_press', key='control') + do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) + if not use_default_state: + do_event(tool, 'on_key_release', key='control') + do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) + assert tool.extents == (70.0 - xdiff, xdata_new, 65.0 - ydiff, ydata_new) + + # resize E handle + extents = tool.extents + xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = 10 + xdata_new, ydata_new = xdata + xdiff, ydata + do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) + if not use_default_state: + do_event(tool, 'on_key_press', key='control') + do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) + do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) + if not use_default_state: + do_event(tool, 'on_key_release', key='control') + assert tool.extents == (60.0 - xdiff, xdata_new, 60.0, 135.0) + + # resize E handle negative diff + extents = tool.extents + xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = -20 + xdata_new, ydata_new = xdata + xdiff, ydata + do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) + if not use_default_state: + do_event(tool, 'on_key_press', key='control') + do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) + do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) + if not use_default_state: + do_event(tool, 'on_key_release', key='control') + assert tool.extents == (50.0 - xdiff, xdata_new, 60.0, 135.0) + + # resize W handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = 15 + xdata_new, ydata_new = xdata + xdiff, ydata + do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) + if not use_default_state: + do_event(tool, 'on_key_press', key='control') + do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) + do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) + if not use_default_state: + do_event(tool, 'on_key_release', key='control') + assert tool.extents == (xdata_new, 125.0 - xdiff, 60.0, 135.0) + + # resize W handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = -25 + xdata_new, ydata_new = xdata + xdiff, ydata + do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) + if not use_default_state: + do_event(tool, 'on_key_press', key='control') + do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) + do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) + if not use_default_state: + do_event(tool, 'on_key_release', key='control') + assert tool.extents == (xdata_new, 110.0 - xdiff, 60.0, 135.0) + + # resize SW handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + xdiff, ydiff = 20, 25 + xdata_new, ydata_new = xdata + xdiff, ydata + ydiff + do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) + if not use_default_state: + do_event(tool, 'on_key_press', key='control') + do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) + do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) + if not use_default_state: + do_event(tool, 'on_key_release', key='control') + assert tool.extents == (xdata_new, 135.0 - xdiff, ydata_new, 135.0 - ydiff) + + def test_ellipse(): """For ellipse, test out the key modifiers""" ax = get_ax() diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index c63ee651869c..59857c95bde7 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1829,6 +1829,7 @@ def __init__(self, ax, onselect, useblit=False, button=None, self._eventrelease = None self._prev_event = None self._state = set() + self._default_state = set() eventpress = _api.deprecate_privatize_attribute("3.5") eventrelease = _api.deprecate_privatize_attribute("3.5") @@ -2965,20 +2966,48 @@ def _release(self, event): def _onmove(self, event): """Motion notify event handler.""" - + state = self._state | self._default_state # resize an existing shape if self._active_handle and self._active_handle != 'C': x0, x1, y0, y1 = self._extents_on_press - # Switch variables so that only x1 and/or y1 are updated on move. - if self._active_handle in ['W', 'SW', 'NW']: - x0, x1 = x1, event.xdata - if self._active_handle in ['S', 'SW', 'SE']: - y0, y1 = y1, event.ydata + if len(state) == 0: + # Switch variables so that only x1 and/or y1 are updated on move. + if self._active_handle in ['W', 'SW', 'NW']: + x0, x1 = x1, event.xdata + if self._active_handle in ['S', 'SW', 'SE']: + y0, y1 = y1, event.ydata + + if self._active_handle in ['E', 'W'] + self._corner_order: + x1 = event.xdata + if self._active_handle in ['N', 'S'] + self._corner_order: + y1 = event.ydata + + else: + dx = event.xdata - self._eventpress.xdata + dy = event.ydata - self._eventpress.ydata + + sizepress = [x1 - x0, y1 - y0] + centerpress = [x0 + sizepress[0] / 2, y0 + sizepress[1] / 2] + center = centerpress + + # from center + if 'center' in state: + if 'W' in self._active_handle: + dx = -dx + if 'S' in self._active_handle: + dy = -dy + dw = sizepress[0] / 2 + dx + dh = sizepress[1] / 2 + dy + + # cancel changes in perpendicular direction + if self._active_handle in ['E', 'W']: + dh = sizepress[1] / 2 + if self._active_handle in ['N', 'S']: + dw = sizepress[0] / 2 + + x0, x1, y0, y1 = (center[0] - dw, center[0] + dw, + center[1] - dh, center[1] + dh) - if self._active_handle in ['E', 'W'] + self._corner_order: - x1 = event.xdata - if self._active_handle in ['N', 'S'] + self._corner_order: - y1 = event.ydata # move existing shape elif (self._active_handle == 'C' or @@ -3004,7 +3033,7 @@ def _onmove(self, event): dy = (event.ydata - center[1]) / 2. # square shape - if 'square' in self._state: + if 'square' in state: dx_pix = abs(event.x - center_pix[0]) dy_pix = abs(event.y - center_pix[1]) if not dx_pix: @@ -3016,7 +3045,7 @@ def _onmove(self, event): dy *= maxd / (abs(dy_pix) + 1e-6) # from center - if 'center' in self._state: + if 'center' in state: dx *= 2 dy *= 2 From a2f62de3d444d9847805c1d6bff4b3657d4f80e0 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 14 Aug 2021 16:28:02 +0100 Subject: [PATCH 3/5] Fix square modifier in interactive rectangle selector - in _onmove of existing shape, part 1, with centering on --- lib/matplotlib/widgets.py | 69 ++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 59857c95bde7..280115dd54f1 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2970,35 +2970,34 @@ def _onmove(self, event): # resize an existing shape if self._active_handle and self._active_handle != 'C': x0, x1, y0, y1 = self._extents_on_press - if len(state) == 0: - # Switch variables so that only x1 and/or y1 are updated on move. - if self._active_handle in ['W', 'SW', 'NW']: - x0, x1 = x1, event.xdata - if self._active_handle in ['S', 'SW', 'SE']: - y0, y1 = y1, event.ydata - - if self._active_handle in ['E', 'W'] + self._corner_order: - x1 = event.xdata - if self._active_handle in ['N', 'S'] + self._corner_order: - y1 = event.ydata - - else: - dx = event.xdata - self._eventpress.xdata - dy = event.ydata - self._eventpress.ydata + dx = event.xdata - self._eventpress.xdata + dy = event.ydata - self._eventpress.ydata + # from center + if 'center' in state: sizepress = [x1 - x0, y1 - y0] centerpress = [x0 + sizepress[0] / 2, y0 + sizepress[1] / 2] center = centerpress - - # from center - if 'center' in state: - if 'W' in self._active_handle: - dx = -dx - if 'S' in self._active_handle: - dy = -dy - dw = sizepress[0] / 2 + dx - dh = sizepress[1] / 2 + dy - + if 'square' in state: + if self._active_handle in ['E', 'W']: + # using E, W handle we need to update dy accordingly + dy = dx if self._active_handle == 'E' else -dx + elif self._active_handle in ['S', 'N']: + # using S, N handle, we need to update dx accordingly + dx = dy if self._active_handle == 'N' else -dy + elif self._active_handle in self._corner_order: + # This doesn't work + dx = dy = max(dx, dy) + + if 'W' in self._active_handle: + dx *= -1 + if 'S' in self._active_handle: + dy *= -1 + + dw = sizepress[0] / 2 + dx + dh = sizepress[1] / 2 + dy + + if 'square' not in state: # cancel changes in perpendicular direction if self._active_handle in ['E', 'W']: dh = sizepress[1] / 2 @@ -3007,7 +3006,25 @@ def _onmove(self, event): x0, x1, y0, y1 = (center[0] - dw, center[0] + dw, center[1] - dh, center[1] + dh) - + else: + # Switch variables so that only x1 and/or y1 are updated on move + if 'W' in self._active_handle: + x0 = x1 + if 'S' in self._active_handle: + y0 = y1 + + if self._active_handle in ['E', 'W'] + self._corner_order: + x1 = event.xdata + if 'square' in state: + # update perpendicular direction + dy = dx if self._active_handle == 'E' else -dx + y1 += dy + if self._active_handle in ['N', 'S'] + self._corner_order: + y1 = event.ydata + if 'square' in state: + # update perpendicular direction + dx = dy if self._active_handle == 'N' else -dy + x1 += dx # move existing shape elif (self._active_handle == 'C' or From 05f5266678bddfc9d3b6dfe1bcf8819cbcc3bb6d Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 14 Aug 2021 18:21:06 +0100 Subject: [PATCH 4/5] Fix square modifier in interactive rectangle selector - in _onmove of existing shape, part 2, with centering off --- lib/matplotlib/tests/test_widgets.py | 255 +++++++++++++++++++-------- lib/matplotlib/widgets.py | 59 +++---- 2 files changed, 213 insertions(+), 101 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 0f604524fb72..9955aad0512d 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -55,6 +55,19 @@ def test_rectangle_selector(): check_rectangle(props=dict(fill=True)) +def _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new, + use_key=None): + do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) + if use_key is not None: + do_event(tool, 'on_key_press', key=use_key) + do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) + if use_key is not None: + do_event(tool, 'on_key_release', key=use_key) + do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) + + return tool + + @pytest.mark.parametrize('drag_from_anywhere, new_center', [[True, (60, 75)], [False, (30, 20)]]) @@ -126,46 +139,36 @@ def onselect(epress, erelease): tool = widgets.RectangleSelector(ax, onselect, interactive=True) # Create rectangle - do_event(tool, 'press', xdata=0, ydata=10, button=1) - do_event(tool, 'onmove', xdata=100, ydata=120, button=1) - do_event(tool, 'release', xdata=100, ydata=120, button=1) + _resize_rectangle(tool, 0, 10, 100, 120) assert tool.extents == (0.0, 100.0, 10.0, 120.0) # resize NE handle extents = tool.extents xdata, ydata = extents[1], extents[3] xdata_new, ydata_new = xdata + 10, ydata + 5 - do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) - do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) - do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) - assert tool.extents == (0.0, xdata_new, 10.0, ydata_new) + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new) + assert tool.extents == (extents[0], xdata_new, extents[2], ydata_new) # resize E handle extents = tool.extents xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 xdata_new, ydata_new = xdata + 10, ydata - do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) - do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) - do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) - assert tool.extents == (0.0, xdata_new, 10.0, 125.0) + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new) + assert tool.extents == (extents[0], xdata_new, extents[2], extents[3]) # resize W handle extents = tool.extents xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 xdata_new, ydata_new = xdata + 15, ydata - do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) - do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) - do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) - assert tool.extents == (xdata_new, 120.0, 10.0, 125.0) + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new) + assert tool.extents == (xdata_new, extents[1], extents[2], extents[3]) # resize SW handle extents = tool.extents xdata, ydata = extents[0], extents[2] xdata_new, ydata_new = xdata + 20, ydata + 25 - do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) - do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) - do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) - assert tool.extents == (xdata_new, 120.0, ydata_new, 125.0) + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new) + assert tool.extents == (xdata_new, extents[1], ydata_new, extents[3]) @pytest.mark.parametrize('use_default_state', [True, False]) @@ -177,97 +180,209 @@ def onselect(epress, erelease): tool = widgets.RectangleSelector(ax, onselect, interactive=True) # Create rectangle - do_event(tool, 'press', xdata=70, ydata=65, button=1) - do_event(tool, 'onmove', xdata=125, ydata=130, button=1) - do_event(tool, 'release', xdata=125, ydata=130, button=1) + _resize_rectangle(tool, 70, 65, 125, 130) assert tool.extents == (70.0, 125.0, 65.0, 130.0) if use_default_state: tool._default_state.add('center') + use_key = None + else: + use_key = 'control' # resize NE handle extents = tool.extents xdata, ydata = extents[1], extents[3] xdiff, ydiff = 10, 5 xdata_new, ydata_new = xdata + xdiff, ydata + ydiff - do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) - if not use_default_state: - do_event(tool, 'on_key_press', key='control') - do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) - if not use_default_state: - do_event(tool, 'on_key_release', key='control') - do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) - assert tool.extents == (70.0 - xdiff, xdata_new, 65.0 - ydiff, ydata_new) + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new, use_key) + assert tool.extents == (extents[0] - xdiff, xdata_new, + extents[2] - ydiff, ydata_new) # resize E handle extents = tool.extents xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 xdiff = 10 xdata_new, ydata_new = xdata + xdiff, ydata - do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) - if not use_default_state: - do_event(tool, 'on_key_press', key='control') - do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) - do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) - if not use_default_state: - do_event(tool, 'on_key_release', key='control') - assert tool.extents == (60.0 - xdiff, xdata_new, 60.0, 135.0) + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new, use_key) + assert tool.extents == (extents[0] - xdiff, xdata_new, + extents[2], extents[3]) # resize E handle negative diff extents = tool.extents xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 xdiff = -20 xdata_new, ydata_new = xdata + xdiff, ydata - do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) - if not use_default_state: - do_event(tool, 'on_key_press', key='control') - do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) - do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) - if not use_default_state: - do_event(tool, 'on_key_release', key='control') - assert tool.extents == (50.0 - xdiff, xdata_new, 60.0, 135.0) + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new, use_key) + assert tool.extents == (extents[0] - xdiff, xdata_new, + extents[2], extents[3]) # resize W handle extents = tool.extents xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 xdiff = 15 xdata_new, ydata_new = xdata + xdiff, ydata - do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) - if not use_default_state: - do_event(tool, 'on_key_press', key='control') - do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) - do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) - if not use_default_state: - do_event(tool, 'on_key_release', key='control') - assert tool.extents == (xdata_new, 125.0 - xdiff, 60.0, 135.0) + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new, use_key) + assert tool.extents == (xdata_new, extents[1] - xdiff, + extents[2], extents[3]) + + # resize W handle negative diff + extents = tool.extents + xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = -25 + xdata_new, ydata_new = xdata + xdiff, ydata + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new, use_key) + assert tool.extents == (xdata_new, extents[1] - xdiff, + extents[2], extents[3]) + + # resize SW handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + xdiff, ydiff = 20, 25 + xdata_new, ydata_new = xdata + xdiff, ydata + ydiff + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new, use_key) + assert tool.extents == (xdata_new, extents[1] - xdiff, + ydata_new, extents[3] - ydiff) + + +@pytest.mark.parametrize('use_default_state', [True, False]) +def test_rectangle_resize_square(use_default_state): + ax = get_ax() + + def onselect(epress, erelease): + pass + + tool = widgets.RectangleSelector(ax, onselect, interactive=True) + # Create rectangle + _resize_rectangle(tool, 70, 65, 120, 115) + assert tool.extents == (70.0, 120.0, 65.0, 115.0) + + if use_default_state: + tool._default_state.add('square') + use_key = None + else: + use_key = 'shift' + + # resize NE handle + extents = tool.extents + xdata, ydata = extents[1], extents[3] + xdiff, ydiff = 10, 5 + xdata_new, ydata_new = xdata + xdiff, ydata + ydiff + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new, use_key) + assert tool.extents == (extents[0], xdata_new, + extents[2], extents[3] + xdiff) + + # resize E handle + extents = tool.extents + xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = 10 + xdata_new, ydata_new = xdata + xdiff, ydata + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new, use_key) + assert tool.extents == (extents[0], xdata_new, + extents[2], extents[3] + xdiff) + + # resize E handle negative diff + extents = tool.extents + xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = -20 + xdata_new, ydata_new = xdata + xdiff, ydata + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new, use_key) + assert tool.extents == (extents[0], xdata_new, + extents[2], extents[3] + xdiff) # resize W handle extents = tool.extents xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = 15 + xdata_new, ydata_new = xdata + xdiff, ydata + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new, use_key) + assert tool.extents == (xdata_new, extents[1], + extents[2], extents[3] - xdiff) + + # resize W handle negative diff + extents = tool.extents + xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 xdiff = -25 xdata_new, ydata_new = xdata + xdiff, ydata - do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) - if not use_default_state: - do_event(tool, 'on_key_press', key='control') - do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) - do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) - if not use_default_state: - do_event(tool, 'on_key_release', key='control') - assert tool.extents == (xdata_new, 110.0 - xdiff, 60.0, 135.0) + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new, use_key) + assert tool.extents == (xdata_new, extents[1], + extents[2], extents[3] - xdiff) # resize SW handle extents = tool.extents xdata, ydata = extents[0], extents[2] xdiff, ydiff = 20, 25 xdata_new, ydata_new = xdata + xdiff, ydata + ydiff - do_event(tool, 'press', xdata=xdata, ydata=ydata, button=1) - if not use_default_state: - do_event(tool, 'on_key_press', key='control') - do_event(tool, 'onmove', xdata=xdata_new, ydata=ydata_new, button=1) - do_event(tool, 'release', xdata=xdata_new, ydata=ydata_new, button=1) - if not use_default_state: - do_event(tool, 'on_key_release', key='control') - assert tool.extents == (xdata_new, 135.0 - xdiff, ydata_new, 135.0 - ydiff) + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new, use_key) + assert tool.extents == (extents[0] + ydiff, extents[1], + ydata_new, extents[3]) + + +def test_rectangle_resize_square_center(): + ax = get_ax() + + def onselect(epress, erelease): + pass + + tool = widgets.RectangleSelector(ax, onselect, interactive=True) + # Create rectangle + _resize_rectangle(tool, 70, 65, 120, 115) + tool._default_state.add('square') + tool._default_state.add('center') + assert tool.extents == (70.0, 120.0, 65.0, 115.0) + + # resize NE handle + extents = tool.extents + xdata, ydata = extents[1], extents[3] + xdiff, ydiff = 10, 5 + xdata_new, ydata_new = xdata + xdiff, ydata + ydiff + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new) + assert tool.extents == (extents[0] - xdiff, xdata_new, + extents[2] - xdiff, extents[3] + xdiff) + + # resize E handle + extents = tool.extents + xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = 10 + xdata_new, ydata_new = xdata + xdiff, ydata + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new) + assert tool.extents == (extents[0] - xdiff, xdata_new, + extents[2] - xdiff, extents[3] + xdiff) + + # resize E handle negative diff + extents = tool.extents + xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = -20 + xdata_new, ydata_new = xdata + xdiff, ydata + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new) + assert tool.extents == (extents[0] - xdiff, xdata_new, + extents[2] - xdiff, extents[3] + xdiff) + + # resize W handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = 5 + xdata_new, ydata_new = xdata + xdiff, ydata + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new) + assert tool.extents == (xdata_new, extents[1] - xdiff, + extents[2] + xdiff, extents[3] - xdiff) + + # resize W handle negative diff + extents = tool.extents + xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = -25 + xdata_new, ydata_new = xdata + xdiff, ydata + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new) + assert tool.extents == (xdata_new, extents[1] - xdiff, + extents[2] + xdiff, extents[3] - xdiff) + + # resize SW handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + xdiff, ydiff = 20, 25 + xdata_new, ydata_new = xdata + xdiff, ydata + ydiff + _resize_rectangle(tool, xdata, ydata, xdata_new, ydata_new) + assert tool.extents == (extents[0] + ydiff, extents[1] - ydiff, + ydata_new, extents[3] - ydiff) def test_ellipse(): diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 280115dd54f1..a48481f591a6 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2970,29 +2970,34 @@ def _onmove(self, event): # resize an existing shape if self._active_handle and self._active_handle != 'C': x0, x1, y0, y1 = self._extents_on_press + sizepress = [x1 - x0, y1 - y0] + center = [x0 + sizepress[0] / 2, y0 + sizepress[1] / 2] dx = event.xdata - self._eventpress.xdata dy = event.ydata - self._eventpress.ydata + # change sign of relative changes to simplify calculation + # Switch variables so that only x1 and/or y1 are updated on move + x_factor = y_factor = 1 + if 'W' in self._active_handle: + x_factor *= -1 + dx *= x_factor + x0 = x1 + if 'S' in self._active_handle: + y_factor *= -1 + dy *= y_factor + y0 = y1 + # from center if 'center' in state: - sizepress = [x1 - x0, y1 - y0] - centerpress = [x0 + sizepress[0] / 2, y0 + sizepress[1] / 2] - center = centerpress if 'square' in state: if self._active_handle in ['E', 'W']: # using E, W handle we need to update dy accordingly - dy = dx if self._active_handle == 'E' else -dx + dy = dx elif self._active_handle in ['S', 'N']: # using S, N handle, we need to update dx accordingly - dx = dy if self._active_handle == 'N' else -dy - elif self._active_handle in self._corner_order: - # This doesn't work - dx = dy = max(dx, dy) - - if 'W' in self._active_handle: - dx *= -1 - if 'S' in self._active_handle: - dy *= -1 + dx = dy + else: + dx = dy = max(dx, dy, key=abs) dw = sizepress[0] / 2 + dx dh = sizepress[1] / 2 + dy @@ -3006,25 +3011,17 @@ def _onmove(self, event): x0, x1, y0, y1 = (center[0] - dw, center[0] + dw, center[1] - dh, center[1] + dh) + else: - # Switch variables so that only x1 and/or y1 are updated on move - if 'W' in self._active_handle: - x0 = x1 - if 'S' in self._active_handle: - y0 = y1 - - if self._active_handle in ['E', 'W'] + self._corner_order: - x1 = event.xdata - if 'square' in state: - # update perpendicular direction - dy = dx if self._active_handle == 'E' else -dx - y1 += dy - if self._active_handle in ['N', 'S'] + self._corner_order: - y1 = event.ydata - if 'square' in state: - # update perpendicular direction - dx = dy if self._active_handle == 'N' else -dy - x1 += dx + if 'square' in state: + dx = dy = max(dx, dy, key=abs) + x1 = x0 + x_factor * (dx + sizepress[0]) + y1 = y0 + y_factor * (dy + sizepress[1]) + else: + if self._active_handle in ['E', 'W'] + self._corner_order: + x1 = event.xdata + if self._active_handle in ['N', 'S'] + self._corner_order: + y1 = event.ydata # move existing shape elif (self._active_handle == 'C' or From 133e100c4f96688af8dbe1c3543891fdb2c164e8 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Thu, 11 Nov 2021 19:04:46 +0000 Subject: [PATCH 5/5] Add comments and rename variable to a more explicit name --- lib/matplotlib/widgets.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index a48481f591a6..34e6d9a4a948 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2970,8 +2970,8 @@ def _onmove(self, event): # resize an existing shape if self._active_handle and self._active_handle != 'C': x0, x1, y0, y1 = self._extents_on_press - sizepress = [x1 - x0, y1 - y0] - center = [x0 + sizepress[0] / 2, y0 + sizepress[1] / 2] + size_on_press = [x1 - x0, y1 - y0] + center = [x0 + size_on_press[0] / 2, y0 + size_on_press[1] / 2] dx = event.xdata - self._eventpress.xdata dy = event.ydata - self._eventpress.ydata @@ -2987,9 +2987,10 @@ def _onmove(self, event): dy *= y_factor y0 = y1 - # from center + # Keeping the center fixed if 'center' in state: if 'square' in state: + # Force the same change in dx and dy if self._active_handle in ['E', 'W']: # using E, W handle we need to update dy accordingly dy = dx @@ -2999,24 +3000,26 @@ def _onmove(self, event): else: dx = dy = max(dx, dy, key=abs) - dw = sizepress[0] / 2 + dx - dh = sizepress[1] / 2 + dy + # new half-width and half-height + hw = size_on_press[0] / 2 + dx + hh = size_on_press[1] / 2 + dy if 'square' not in state: # cancel changes in perpendicular direction if self._active_handle in ['E', 'W']: - dh = sizepress[1] / 2 + hh = size_on_press[1] / 2 if self._active_handle in ['N', 'S']: - dw = sizepress[0] / 2 + hw = size_on_press[0] / 2 - x0, x1, y0, y1 = (center[0] - dw, center[0] + dw, - center[1] - dh, center[1] + dh) + x0, x1, y0, y1 = (center[0] - hw, center[0] + hw, + center[1] - hh, center[1] + hh) else: + # Keeping the opposite corner/edge fixed if 'square' in state: dx = dy = max(dx, dy, key=abs) - x1 = x0 + x_factor * (dx + sizepress[0]) - y1 = y0 + y_factor * (dy + sizepress[1]) + x1 = x0 + x_factor * (dx + size_on_press[0]) + y1 = y0 + y_factor * (dy + size_on_press[1]) else: if self._active_handle in ['E', 'W'] + self._corner_order: x1 = event.xdata