20
20
from . import _api , backend_tools , cbook , colors , ticker
21
21
from .lines import Line2D
22
22
from .patches import Circle , Rectangle , Ellipse
23
+ from .transforms import Affine2D
23
24
24
25
25
26
class LockDraw :
@@ -1797,7 +1798,8 @@ def __init__(self, ax, onselect, useblit=False, button=None,
1797
1798
1798
1799
self ._state_modifier_keys = dict (move = ' ' , clear = 'escape' ,
1799
1800
square = 'shift' , center = 'control' ,
1800
- data_coordinates = 'd' )
1801
+ data_coordinates = 'd' ,
1802
+ rotate = 'r' )
1801
1803
self ._state_modifier_keys .update (state_modifier_keys or {})
1802
1804
1803
1805
self .background = None
@@ -1932,8 +1934,9 @@ def press(self, event):
1932
1934
key = event .key or ''
1933
1935
key = key .replace ('ctrl' , 'control' )
1934
1936
# move state is locked in on a button press
1935
- if key == self ._state_modifier_keys ['move' ]:
1936
- self ._state .add ('move' )
1937
+ for action in ['move' ]:
1938
+ if key == self ._state_modifier_keys [action ]:
1939
+ self ._state .add (action )
1937
1940
self ._press (event )
1938
1941
return True
1939
1942
return False
@@ -1985,14 +1988,15 @@ def on_key_press(self, event):
1985
1988
artist .set_visible (False )
1986
1989
self .update ()
1987
1990
return
1988
- if key == 'd' and key in self . _state_modifier_keys . values () :
1989
- modifier = 'data_coordinates'
1990
- if modifier in self ._default_state :
1991
- self ._default_state .remove (modifier )
1992
- else :
1993
- self .add_default_state (modifier )
1991
+ for state in [ 'rotate' , 'data_coordinates' ] :
1992
+ if key == self . _state_modifier_keys [ state ]:
1993
+ if state in self ._default_state :
1994
+ self ._default_state .remove (state )
1995
+ else :
1996
+ self .add_default_state (state )
1994
1997
for (state , modifier ) in self ._state_modifier_keys .items ():
1995
- if modifier in key :
1998
+ # Multiple keys are string concatenated using '+'
1999
+ if modifier in key .split ('+' ):
1996
2000
self ._state .add (state )
1997
2001
self ._on_key_press (event )
1998
2002
@@ -2193,7 +2197,8 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False,
2193
2197
state_modifier_keys = dict (clear = 'escape' ,
2194
2198
square = 'not-applicable' ,
2195
2199
center = 'not-applicable' ,
2196
- data_coordinates = 'not-applicable' )
2200
+ data_coordinates = 'not-applicable' ,
2201
+ rotate = 'not-applicable' )
2197
2202
super ().__init__ (ax , onselect , useblit = useblit , button = button ,
2198
2203
state_modifier_keys = state_modifier_keys )
2199
2204
@@ -2764,6 +2769,7 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent)
2764
2769
- "center": change the shape around its center, default: "ctrl".
2765
2770
- "data_coordinates": define if data or figure coordinates should be
2766
2771
used to define the square shape, default: "d"
2772
+ - "rotate": Rotate the shape around its center, default: "r".
2767
2773
2768
2774
"square" and "center" can be combined. The square shape can be defined
2769
2775
in data or figure coordinates as determined by the ``data_coordinates``
@@ -2811,8 +2817,6 @@ class RectangleSelector(_SelectorWidget):
2811
2817
See also: :doc:`/gallery/widgets/rectangle_selector`
2812
2818
"""
2813
2819
2814
- _shape_klass = Rectangle
2815
-
2816
2820
@_api .rename_parameter ("3.5" , "maxdist" , "grab_range" )
2817
2821
@_api .rename_parameter ("3.5" , "marker_props" , "handle_props" )
2818
2822
@_api .rename_parameter ("3.5" , "rectprops" , "props" )
@@ -2831,6 +2835,7 @@ def __init__(self, ax, onselect, drawtype='box',
2831
2835
self ._interactive = interactive
2832
2836
self .drag_from_anywhere = drag_from_anywhere
2833
2837
self .ignore_event_outside = ignore_event_outside
2838
+ self ._rotation = 0
2834
2839
2835
2840
if drawtype == 'none' : # draw a line but make it invisible
2836
2841
_api .warn_deprecated (
@@ -2848,8 +2853,7 @@ def __init__(self, ax, onselect, drawtype='box',
2848
2853
props ['animated' ] = self .useblit
2849
2854
self .visible = props .pop ('visible' , self .visible )
2850
2855
self ._props = props
2851
- to_draw = self ._shape_klass ((0 , 0 ), 0 , 1 , visible = False ,
2852
- ** self ._props )
2856
+ to_draw = self ._init_shape (** self ._props )
2853
2857
self .ax .add_patch (to_draw )
2854
2858
if drawtype == 'line' :
2855
2859
_api .warn_deprecated (
@@ -2921,6 +2925,10 @@ def _handles_artists(self):
2921
2925
return (* self ._center_handle .artists , * self ._corner_handles .artists ,
2922
2926
* self ._edge_handles .artists )
2923
2927
2928
+ def _init_shape (self , ** props ):
2929
+ return Rectangle ((0 , 0 ), 0 , 1 , visible = False ,
2930
+ rotate_around_center = True , ** props )
2931
+
2924
2932
def _press (self , event ):
2925
2933
"""Button press event handler."""
2926
2934
# make the drawn box/line visible get the click-coordinates,
@@ -3017,9 +3025,17 @@ def _onmove(self, event):
3017
3025
refx = event .xdata / (self ._eventpress .xdata + 1e-6 )
3018
3026
refy = event .ydata / (self ._eventpress .ydata + 1e-6 )
3019
3027
3028
+
3029
+ x0 , x1 , y0 , y1 = self ._extents_on_press
3020
3030
# resize an existing shape
3021
- if self ._active_handle and self ._active_handle != 'C' :
3022
- x0 , x1 , y0 , y1 = self ._extents_on_press
3031
+ if 'rotate' in state and self ._active_handle in self ._corner_order :
3032
+ # calculate angle abc
3033
+ a = np .array ([self ._eventpress .xdata , self ._eventpress .ydata ])
3034
+ b = np .array (self .center )
3035
+ c = np .array ([event .xdata , event .ydata ])
3036
+ self ._rotation = (np .arctan2 (c [1 ]- b [1 ], c [0 ]- b [0 ]) -
3037
+ np .arctan2 (a [1 ]- b [1 ], a [0 ]- b [0 ]))
3038
+ elif self ._active_handle and self ._active_handle != 'C' :
3023
3039
sizepress = [x1 - x0 , y1 - y0 ]
3024
3040
center = [x0 + sizepress [0 ] / 2 , y0 + sizepress [1 ] / 2 ]
3025
3041
@@ -3084,6 +3100,7 @@ def _onmove(self, event):
3084
3100
3085
3101
# new shape
3086
3102
else :
3103
+ self ._rotation = 0
3087
3104
# Don't create a new rectangle if there is already one when
3088
3105
# ignore_event_outside=True
3089
3106
if self .ignore_event_outside and self ._selection_completed :
@@ -3118,24 +3135,25 @@ def _onmove(self, event):
3118
3135
@property
3119
3136
def _rect_bbox (self ):
3120
3137
if self ._drawtype == 'box' :
3121
- x0 = self ._selection_artist .get_x ()
3122
- y0 = self ._selection_artist .get_y ()
3123
- width = self ._selection_artist .get_width ()
3124
- height = self ._selection_artist .get_height ()
3125
- return x0 , y0 , width , height
3138
+ return self ._selection_artist .get_bbox ().bounds
3126
3139
else :
3127
3140
x , y = self ._selection_artist .get_data ()
3128
3141
x0 , x1 = min (x ), max (x )
3129
3142
y0 , y1 = min (y ), max (y )
3130
3143
return x0 , y0 , x1 - x0 , y1 - y0
3131
3144
3145
+ def _get_rotation_transform (self ):
3146
+ return Affine2D ().rotate_around (* self .center , self ._rotation )
3147
+
3132
3148
@property
3133
3149
def corners (self ):
3134
3150
"""Corners of rectangle from lower left, moving clockwise."""
3135
3151
x0 , y0 , width , height = self ._rect_bbox
3136
3152
xc = x0 , x0 + width , x0 + width , x0
3137
3153
yc = y0 , y0 , y0 + height , y0 + height
3138
- return xc , yc
3154
+ transform = self ._get_rotation_transform ()
3155
+ coords = transform .transform (np .array ([xc , yc ]).T ).T
3156
+ return coords [0 ], coords [1 ]
3139
3157
3140
3158
@property
3141
3159
def edge_centers (self ):
@@ -3145,7 +3163,9 @@ def edge_centers(self):
3145
3163
h = height / 2.
3146
3164
xe = x0 , x0 + w , x0 + width , x0 + w
3147
3165
ye = y0 + h , y0 , y0 + h , y0 + height
3148
- return xe , ye
3166
+ transform = self ._get_rotation_transform ()
3167
+ coords = transform .transform (np .array ([xe , ye ]).T ).T
3168
+ return coords [0 ], coords [1 ]
3149
3169
3150
3170
@property
3151
3171
def center (self ):
@@ -3155,7 +3175,10 @@ def center(self):
3155
3175
3156
3176
@property
3157
3177
def extents (self ):
3158
- """Return (xmin, xmax, ymin, ymax)."""
3178
+ """
3179
+ Return (xmin, xmax, ymin, ymax) as defined by the bounding box before
3180
+ rotation.
3181
+ """
3159
3182
x0 , y0 , width , height = self ._rect_bbox
3160
3183
xmin , xmax = sorted ([x0 , x0 + width ])
3161
3184
ymin , ymax = sorted ([y0 , y0 + height ])
@@ -3173,6 +3196,17 @@ def extents(self, extents):
3173
3196
self .set_visible (self .visible )
3174
3197
self .update ()
3175
3198
3199
+ @property
3200
+ def rotation (self ):
3201
+ """Rotation in degree."""
3202
+ return np .rad2deg (self ._rotation )
3203
+
3204
+ @rotation .setter
3205
+ def rotation (self , value ):
3206
+ self ._rotation = np .deg2rad (value )
3207
+ # call extents setter to draw shape and update handles positions
3208
+ self .extents = self .extents
3209
+
3176
3210
draw_shape = _api .deprecate_privatize_attribute ('3.5' )
3177
3211
3178
3212
def _draw_shape (self , extents ):
@@ -3192,6 +3226,7 @@ def _draw_shape(self, extents):
3192
3226
self ._selection_artist .set_y (ymin )
3193
3227
self ._selection_artist .set_width (xmax - xmin )
3194
3228
self ._selection_artist .set_height (ymax - ymin )
3229
+ self ._selection_artist .set_angle (self .rotation )
3195
3230
3196
3231
elif self ._drawtype == 'line' :
3197
3232
self ._selection_artist .set_data ([xmin , xmax ], [ymin , ymax ])
@@ -3264,9 +3299,11 @@ class EllipseSelector(RectangleSelector):
3264
3299
:doc:`/gallery/widgets/rectangle_selector`
3265
3300
"""
3266
3301
3267
- _shape_klass = Ellipse
3268
3302
draw_shape = _api .deprecate_privatize_attribute ('3.5' )
3269
3303
3304
+ def _init_shape (self , ** props ):
3305
+ return Ellipse ((0 , 0 ), 0 , 1 , visible = False , ** props )
3306
+
3270
3307
def _draw_shape (self , extents ):
3271
3308
x0 , x1 , y0 , y1 = extents
3272
3309
xmin , xmax = sorted ([x0 , x1 ])
@@ -3279,6 +3316,7 @@ def _draw_shape(self, extents):
3279
3316
self ._selection_artist .center = center
3280
3317
self ._selection_artist .width = 2 * a
3281
3318
self ._selection_artist .height = 2 * b
3319
+ self ._selection_artist .set_angle (self .rotation )
3282
3320
else :
3283
3321
rad = np .deg2rad (np .arange (31 ) * 12 )
3284
3322
x = a * np .cos (rad ) + center [0 ]
@@ -3457,7 +3495,8 @@ def __init__(self, ax, onselect, useblit=False,
3457
3495
move_all = 'shift' , move = 'not-applicable' ,
3458
3496
square = 'not-applicable' ,
3459
3497
center = 'not-applicable' ,
3460
- data_coordinates = 'not-applicable' )
3498
+ data_coordinates = 'not-applicable' ,
3499
+ rotate = 'not-applicable' )
3461
3500
super ().__init__ (ax , onselect , useblit = useblit ,
3462
3501
state_modifier_keys = state_modifier_keys )
3463
3502
0 commit comments