Skip to content

Commit 928e662

Browse files
committed
Apply inverse transformation to event so that the onmove calculation are correct
1 parent c748401 commit 928e662

File tree

2 files changed

+73
-23
lines changed

2 files changed

+73
-23
lines changed

lib/matplotlib/tests/test_widgets.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from matplotlib.testing.decorators import check_figures_equal, image_comparison
66
from matplotlib.testing.widgets import do_event, get_ax, mock_event
77

8+
import numpy as np
89
from numpy.testing import assert_allclose
910

1011
import pytest
@@ -406,6 +407,42 @@ def onselect(epress, erelease):
406407
ydata_new, extents[3] - ydiff)
407408

408409

410+
@pytest.mark.parametrize('selector_class',
411+
[widgets.RectangleSelector, widgets.EllipseSelector])
412+
def test_rectangle_rotate(selector_class):
413+
ax = get_ax()
414+
415+
def onselect(epress, erelease):
416+
pass
417+
418+
tool = selector_class(ax, onselect=onselect, interactive=True)
419+
# Draw rectangle
420+
do_event(tool, 'press', xdata=100, ydata=100)
421+
do_event(tool, 'onmove', xdata=130, ydata=140)
422+
do_event(tool, 'release', xdata=130, ydata=140)
423+
assert tool.extents == (100, 130, 100, 140)
424+
425+
# Rotate anticlockwise using top-right corner
426+
do_event(tool, 'on_key_press', key='r')
427+
do_event(tool, 'press', xdata=130, ydata=140)
428+
do_event(tool, 'onmove', xdata=110, ydata=145)
429+
do_event(tool, 'release', xdata=110, ydata=145)
430+
do_event(tool, 'on_key_press', key='r')
431+
# Extents shouldn't change (as shape of rectangle hasn't changed)
432+
assert tool.extents == (100, 130, 100, 140)
433+
# Corners should move
434+
# The third corner is at (100, 145)
435+
assert_allclose(tool.corners,
436+
np.array([[119.9, 139.9, 110.1, 90.1],
437+
[95.4, 117.8, 144.5, 122.2]]), atol=0.1)
438+
439+
# Scale using top-right corner
440+
do_event(tool, 'press', xdata=110, ydata=145)
441+
do_event(tool, 'onmove', xdata=110, ydata=160)
442+
do_event(tool, 'release', xdata=110, ydata=160)
443+
assert_allclose(tool.extents, (100, 141.5, 100, 150.4), atol=0.1)
444+
445+
409446
def test_rectangle_resize_square_center_aspect():
410447
ax = get_ax()
411448
ax.set_aspect(0.8)

lib/matplotlib/widgets.py

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1947,9 +1947,9 @@ def press(self, event):
19471947
key = event.key or ''
19481948
key = key.replace('ctrl', 'control')
19491949
# move state is locked in on a button press
1950-
for action in ['move']:
1951-
if key == self._state_modifier_keys[action]:
1952-
self._state.add(action)
1950+
for state in ['move']:
1951+
if key == self._state_modifier_keys[state]:
1952+
self._state.add(state)
19531953
self._press(event)
19541954
return True
19551955
return False
@@ -1999,16 +1999,15 @@ def on_key_press(self, event):
19991999
if key == self._state_modifier_keys['clear']:
20002000
self.clear()
20012001
return
2002-
for state in ['rotate', 'data_coordinates']:
2003-
if key == self._state_modifier_keys[state]:
2004-
if state in self._default_state:
2005-
self._default_state.remove(state)
2006-
else:
2007-
self.add_default_state(state)
20082002
for (state, modifier) in self._state_modifier_keys.items():
2009-
# Multiple keys are string concatenated using '+'
20102003
if modifier in key.split('+'):
2011-
self._state.add(state)
2004+
# rotate and data_coordinates are enable/disable
2005+
# on key press
2006+
if (state in ['rotate', 'data_coordinates'] and
2007+
state in self._state):
2008+
self._state.discard(state)
2009+
else:
2010+
self._state.add(state)
20122011
self._on_key_press(event)
20132012

20142013
def _on_key_press(self, event):
@@ -2019,7 +2018,8 @@ def on_key_release(self, event):
20192018
if self.active:
20202019
key = event.key or ''
20212020
for (state, modifier) in self._state_modifier_keys.items():
2022-
if modifier in key:
2021+
if (modifier in key.split('+') and
2022+
state not in ['rotate', 'data_coordinates']):
20232023
self._state.discard(state)
20242024
self._on_key_release(event)
20252025

@@ -3033,9 +3033,21 @@ def _onmove(self, event):
30333033
"""Motion notify event handler."""
30343034

30353035
state = self._state | self._default_state
3036+
rotate = ('rotate' in state and
3037+
self._active_handle in self._corner_order)
3038+
eventpress = self._eventpress
3039+
# The calculations are done for rotation at zero: we apply inverse
3040+
# transformation to events except when we rotate and move
3041+
if not (self._active_handle == 'C' or rotate):
3042+
inv_tr = self._get_rotation_transform().inverted()
3043+
event.xdata, event.ydata = inv_tr.transform(
3044+
[event.xdata, event.ydata])
3045+
eventpress.xdata, eventpress.ydata = inv_tr.transform(
3046+
[eventpress.xdata, eventpress.ydata]
3047+
)
30363048

3037-
dx = event.xdata - self._eventpress.xdata
3038-
dy = event.ydata - self._eventpress.ydata
3049+
dx = event.xdata - eventpress.xdata
3050+
dy = event.ydata - eventpress.ydata
30393051
refmax = None
30403052
if 'data_coordinates' in state:
30413053
aspect_ratio = 1
@@ -3045,19 +3057,20 @@ def _onmove(self, event):
30453057
ll, ur = self.ax.get_position() * figure_size
30463058
width, height = ur - ll
30473059
aspect_ratio = height / width * self.ax.get_data_ratio()
3048-
refx = event.xdata / (self._eventpress.xdata + 1e-6)
3049-
refy = event.ydata / (self._eventpress.ydata + 1e-6)
3050-
3060+
refx = event.xdata / (eventpress.xdata + 1e-6)
3061+
refy = event.ydata / (eventpress.ydata + 1e-6)
30513062

30523063
x0, x1, y0, y1 = self._extents_on_press
3053-
# resize an existing shape
3054-
if 'rotate' in state and self._active_handle in self._corner_order:
3064+
# rotate an existing shape
3065+
if rotate:
30553066
# calculate angle abc
3056-
a = np.array([self._eventpress.xdata, self._eventpress.ydata])
3067+
a = np.array([eventpress.xdata, eventpress.ydata])
30573068
b = np.array(self.center)
30583069
c = np.array([event.xdata, event.ydata])
30593070
self._rotation = (np.arctan2(c[1]-b[1], c[0]-b[0]) -
30603071
np.arctan2(a[1]-b[1], a[0]-b[0]))
3072+
3073+
# resize an existing shape
30613074
elif self._active_handle and self._active_handle != 'C':
30623075
size_on_press = [x1 - x0, y1 - y0]
30633076
center = [x0 + size_on_press[0] / 2, y0 + size_on_press[1] / 2]
@@ -3114,8 +3127,8 @@ def _onmove(self, event):
31143127
# move existing shape
31153128
elif self._active_handle == 'C':
31163129
x0, x1, y0, y1 = self._extents_on_press
3117-
dx = event.xdata - self._eventpress.xdata
3118-
dy = event.ydata - self._eventpress.ydata
3130+
dx = event.xdata - eventpress.xdata
3131+
dy = event.ydata - eventpress.ydata
31193132
x0 += dx
31203133
x1 += dx
31213134
y0 += dy
@@ -3128,7 +3141,7 @@ def _onmove(self, event):
31283141
# ignore_event_outside=True
31293142
if self.ignore_event_outside and self._selection_completed:
31303143
return
3131-
center = [self._eventpress.xdata, self._eventpress.ydata]
3144+
center = [eventpress.xdata, eventpress.ydata]
31323145
dx = (event.xdata - center[0]) / 2.
31333146
dy = (event.ydata - center[1]) / 2.
31343147

0 commit comments

Comments
 (0)