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
@@ -1933,8 +1935,9 @@ def press(self, event):
1933
1935
key = event .key or ''
1934
1936
key = key .replace ('ctrl' , 'control' )
1935
1937
# move state is locked in on a button press
1936
- if key == self ._state_modifier_keys ['move' ]:
1937
- self ._state .add ('move' )
1938
+ for action in ['move' ]:
1939
+ if key == self ._state_modifier_keys [action ]:
1940
+ self ._state .add (action )
1938
1941
self ._press (event )
1939
1942
return True
1940
1943
return False
@@ -1986,14 +1989,15 @@ def on_key_press(self, event):
1986
1989
artist .set_visible (False )
1987
1990
self .update ()
1988
1991
return
1989
- if key == 'd' and key in self . _state_modifier_keys . values () :
1990
- modifier = 'data_coordinates'
1991
- if modifier in self ._default_state :
1992
- self ._default_state .remove (modifier )
1993
- else :
1994
- self .add_default_state (modifier )
1992
+ for state in [ 'rotate' , 'data_coordinates' ] :
1993
+ if key == self . _state_modifier_keys [ state ]:
1994
+ if state in self ._default_state :
1995
+ self ._default_state .remove (state )
1996
+ else :
1997
+ self .add_default_state (state )
1995
1998
for (state , modifier ) in self ._state_modifier_keys .items ():
1996
- if modifier in key :
1999
+ # Multiple keys are string concatenated using '+'
2000
+ if modifier in key .split ('+' ):
1997
2001
self ._state .add (state )
1998
2002
self ._on_key_press (event )
1999
2003
@@ -2160,7 +2164,8 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False,
2160
2164
state_modifier_keys = dict (clear = 'escape' ,
2161
2165
square = 'not-applicable' ,
2162
2166
center = 'not-applicable' ,
2163
- data_coordinates = 'not-applicable' )
2167
+ data_coordinates = 'not-applicable' ,
2168
+ rotate = 'not-applicable' )
2164
2169
super ().__init__ (ax , onselect , useblit = useblit , button = button ,
2165
2170
state_modifier_keys = state_modifier_keys )
2166
2171
@@ -2723,6 +2728,7 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent)
2723
2728
- "center": change the shape around its center, default: "ctrl".
2724
2729
- "data_coordinates": define if data or figure coordinates should be
2725
2730
used to define the square shape, default: "d"
2731
+ - "rotate": Rotate the shape around its center, default: "r".
2726
2732
2727
2733
"square" and "center" can be combined. The square shape can be defined
2728
2734
in data or figure coordinates as determined by the ``data_coordinates``
@@ -2770,8 +2776,6 @@ class RectangleSelector(_SelectorWidget):
2770
2776
See also: :doc:`/gallery/widgets/rectangle_selector`
2771
2777
"""
2772
2778
2773
- _shape_klass = Rectangle
2774
-
2775
2779
@_api .rename_parameter ("3.5" , "maxdist" , "grab_range" )
2776
2780
@_api .rename_parameter ("3.5" , "marker_props" , "handle_props" )
2777
2781
@_api .rename_parameter ("3.5" , "rectprops" , "props" )
@@ -2791,6 +2795,7 @@ def __init__(self, ax, onselect, drawtype='box',
2791
2795
self ._interactive = interactive
2792
2796
self .drag_from_anywhere = drag_from_anywhere
2793
2797
self .ignore_event_outside = ignore_event_outside
2798
+ self ._rotation = 0
2794
2799
2795
2800
if drawtype == 'none' : # draw a line but make it invisible
2796
2801
_api .warn_deprecated (
@@ -2808,8 +2813,7 @@ def __init__(self, ax, onselect, drawtype='box',
2808
2813
props ['animated' ] = self .useblit
2809
2814
_props = props
2810
2815
self .visible = _props .pop ('visible' , self .visible )
2811
- self ._to_draw = self ._shape_klass ((0 , 0 ), 0 , 1 , visible = False ,
2812
- ** _props )
2816
+ self ._to_draw = self ._init_shape (** _props )
2813
2817
self .ax .add_patch (self ._to_draw )
2814
2818
if drawtype == 'line' :
2815
2819
_api .warn_deprecated (
@@ -2878,6 +2882,10 @@ def __init__(self, ax, onselect, drawtype='box',
2878
2882
property (lambda self : self .grab_range ,
2879
2883
lambda self , value : setattr (self , "grab_range" , value )))
2880
2884
2885
+ def _init_shape (self , ** props ):
2886
+ return Rectangle ((0 , 0 ), 0 , 1 , visible = False ,
2887
+ rotate_around_center = True , ** props )
2888
+
2881
2889
def _press (self , event ):
2882
2890
"""Button press event handler."""
2883
2891
# make the drawn box/line visible get the click-coordinates,
@@ -2974,9 +2982,17 @@ def _onmove(self, event):
2974
2982
refx = event .xdata / (self ._eventpress .xdata + 1e-6 )
2975
2983
refy = event .ydata / (self ._eventpress .ydata + 1e-6 )
2976
2984
2985
+
2986
+ x0 , x1 , y0 , y1 = self ._extents_on_press
2977
2987
# resize an existing shape
2978
- if self ._active_handle and self ._active_handle != 'C' :
2979
- x0 , x1 , y0 , y1 = self ._extents_on_press
2988
+ if 'rotate' in state and self ._active_handle in self ._corner_order :
2989
+ # calculate angle abc
2990
+ a = np .array ([self ._eventpress .xdata , self ._eventpress .ydata ])
2991
+ b = np .array (self .center )
2992
+ c = np .array ([event .xdata , event .ydata ])
2993
+ self ._rotation = (np .arctan2 (c [1 ]- b [1 ], c [0 ]- b [0 ]) -
2994
+ np .arctan2 (a [1 ]- b [1 ], a [0 ]- b [0 ]))
2995
+ elif self ._active_handle and self ._active_handle != 'C' :
2980
2996
sizepress = [x1 - x0 , y1 - y0 ]
2981
2997
center = [x0 + sizepress [0 ] / 2 , y0 + sizepress [1 ] / 2 ]
2982
2998
@@ -3041,6 +3057,7 @@ def _onmove(self, event):
3041
3057
3042
3058
# new shape
3043
3059
else :
3060
+ self ._rotation = 0
3044
3061
# Don't create a new rectangle if there is already one when
3045
3062
# ignore_event_outside=True
3046
3063
if self .ignore_event_outside and self ._selection_completed :
@@ -3075,24 +3092,25 @@ def _onmove(self, event):
3075
3092
@property
3076
3093
def _rect_bbox (self ):
3077
3094
if self ._drawtype == 'box' :
3078
- x0 = self ._to_draw .get_x ()
3079
- y0 = self ._to_draw .get_y ()
3080
- width = self ._to_draw .get_width ()
3081
- height = self ._to_draw .get_height ()
3082
- return x0 , y0 , width , height
3095
+ return self ._to_draw .get_bbox ().bounds
3083
3096
else :
3084
3097
x , y = self ._to_draw .get_data ()
3085
3098
x0 , x1 = min (x ), max (x )
3086
3099
y0 , y1 = min (y ), max (y )
3087
3100
return x0 , y0 , x1 - x0 , y1 - y0
3088
3101
3102
+ def _get_rotation_transform (self ):
3103
+ return Affine2D ().rotate_around (* self .center , self ._rotation )
3104
+
3089
3105
@property
3090
3106
def corners (self ):
3091
3107
"""Corners of rectangle from lower left, moving clockwise."""
3092
3108
x0 , y0 , width , height = self ._rect_bbox
3093
3109
xc = x0 , x0 + width , x0 + width , x0
3094
3110
yc = y0 , y0 , y0 + height , y0 + height
3095
- return xc , yc
3111
+ transform = self ._get_rotation_transform ()
3112
+ coords = transform .transform (np .array ([xc , yc ]).T ).T
3113
+ return coords [0 ], coords [1 ]
3096
3114
3097
3115
@property
3098
3116
def edge_centers (self ):
@@ -3102,7 +3120,9 @@ def edge_centers(self):
3102
3120
h = height / 2.
3103
3121
xe = x0 , x0 + w , x0 + width , x0 + w
3104
3122
ye = y0 + h , y0 , y0 + h , y0 + height
3105
- return xe , ye
3123
+ transform = self ._get_rotation_transform ()
3124
+ coords = transform .transform (np .array ([xe , ye ]).T ).T
3125
+ return coords [0 ], coords [1 ]
3106
3126
3107
3127
@property
3108
3128
def center (self ):
@@ -3112,7 +3132,10 @@ def center(self):
3112
3132
3113
3133
@property
3114
3134
def extents (self ):
3115
- """Return (xmin, xmax, ymin, ymax)."""
3135
+ """
3136
+ Return (xmin, xmax, ymin, ymax) as defined by the bounding box before
3137
+ rotation.
3138
+ """
3116
3139
x0 , y0 , width , height = self ._rect_bbox
3117
3140
xmin , xmax = sorted ([x0 , x0 + width ])
3118
3141
ymin , ymax = sorted ([y0 , y0 + height ])
@@ -3129,6 +3152,17 @@ def extents(self, extents):
3129
3152
self .set_visible (self .visible )
3130
3153
self .update ()
3131
3154
3155
+ @property
3156
+ def rotation (self ):
3157
+ """Rotation in degree."""
3158
+ return np .rad2deg (self ._rotation )
3159
+
3160
+ @rotation .setter
3161
+ def rotation (self , value ):
3162
+ self ._rotation = np .deg2rad (value )
3163
+ # call extents setter to draw shape and update handles positions
3164
+ self .extents = self .extents
3165
+
3132
3166
draw_shape = _api .deprecate_privatize_attribute ('3.5' )
3133
3167
3134
3168
def _draw_shape (self , extents ):
@@ -3148,6 +3182,7 @@ def _draw_shape(self, extents):
3148
3182
self ._to_draw .set_y (ymin )
3149
3183
self ._to_draw .set_width (xmax - xmin )
3150
3184
self ._to_draw .set_height (ymax - ymin )
3185
+ self ._to_draw .set_angle (self .rotation )
3151
3186
3152
3187
elif self ._drawtype == 'line' :
3153
3188
self ._to_draw .set_data ([xmin , xmax ], [ymin , ymax ])
@@ -3220,9 +3255,11 @@ class EllipseSelector(RectangleSelector):
3220
3255
:doc:`/gallery/widgets/rectangle_selector`
3221
3256
"""
3222
3257
3223
- _shape_klass = Ellipse
3224
3258
draw_shape = _api .deprecate_privatize_attribute ('3.5' )
3225
3259
3260
+ def _init_shape (self , ** props ):
3261
+ return Ellipse ((0 , 0 ), 0 , 1 , visible = False , ** props )
3262
+
3226
3263
def _draw_shape (self , extents ):
3227
3264
x0 , x1 , y0 , y1 = extents
3228
3265
xmin , xmax = sorted ([x0 , x1 ])
@@ -3235,6 +3272,7 @@ def _draw_shape(self, extents):
3235
3272
self ._to_draw .center = center
3236
3273
self ._to_draw .width = 2 * a
3237
3274
self ._to_draw .height = 2 * b
3275
+ self ._to_draw .set_angle (self .rotation )
3238
3276
else :
3239
3277
rad = np .deg2rad (np .arange (31 ) * 12 )
3240
3278
x = a * np .cos (rad ) + center [0 ]
@@ -3412,7 +3450,8 @@ def __init__(self, ax, onselect, useblit=False,
3412
3450
move_all = 'shift' , move = 'not-applicable' ,
3413
3451
square = 'not-applicable' ,
3414
3452
center = 'not-applicable' ,
3415
- data_coordinates = 'not-applicable' )
3453
+ data_coordinates = 'not-applicable' ,
3454
+ rotate = 'not-applicable' )
3416
3455
super ().__init__ (ax , onselect , useblit = useblit ,
3417
3456
state_modifier_keys = state_modifier_keys )
3418
3457
0 commit comments