2020from . import _api , backend_tools , cbook , colors , ticker
2121from .lines import Line2D
2222from .patches import Circle , Rectangle , Ellipse
23- from .transforms import TransformedPatchPath
23+ from .transforms import TransformedPatchPath , Affine2D
2424
2525
2626class LockDraw :
@@ -1811,7 +1811,8 @@ def __init__(self, ax, onselect, useblit=False, button=None,
18111811
18121812 self ._state_modifier_keys = dict (move = ' ' , clear = 'escape' ,
18131813 square = 'shift' , center = 'control' ,
1814- data_coordinates = 'd' )
1814+ data_coordinates = 'd' ,
1815+ rotate = 'r' )
18151816 self ._state_modifier_keys .update (state_modifier_keys or {})
18161817
18171818 self .background = None
@@ -1946,8 +1947,9 @@ def press(self, event):
19461947 key = event .key or ''
19471948 key = key .replace ('ctrl' , 'control' )
19481949 # move state is locked in on a button press
1949- if key == self ._state_modifier_keys ['move' ]:
1950- self ._state .add ('move' )
1950+ for action in ['move' ]:
1951+ if key == self ._state_modifier_keys [action ]:
1952+ self ._state .add (action )
19511953 self ._press (event )
19521954 return True
19531955 return False
@@ -1997,14 +1999,15 @@ def on_key_press(self, event):
19971999 if key == self ._state_modifier_keys ['clear' ]:
19982000 self .clear ()
19992001 return
2000- if key == 'd' and key in self . _state_modifier_keys . values () :
2001- modifier = 'data_coordinates'
2002- if modifier in self ._default_state :
2003- self ._default_state .remove (modifier )
2004- else :
2005- self .add_default_state (modifier )
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 )
20062008 for (state , modifier ) in self ._state_modifier_keys .items ():
2007- if modifier in key :
2009+ # Multiple keys are string concatenated using '+'
2010+ if modifier in key .split ('+' ):
20082011 self ._state .add (state )
20092012 self ._on_key_press (event )
20102013
@@ -2212,7 +2215,8 @@ def __init__(self, ax, onselect, direction, minspan=0, useblit=False,
22122215 state_modifier_keys = dict (clear = 'escape' ,
22132216 square = 'not-applicable' ,
22142217 center = 'not-applicable' ,
2215- data_coordinates = 'not-applicable' )
2218+ data_coordinates = 'not-applicable' ,
2219+ rotate = 'not-applicable' )
22162220 super ().__init__ (ax , onselect , useblit = useblit , button = button ,
22172221 state_modifier_keys = state_modifier_keys )
22182222
@@ -2788,6 +2792,7 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent)
27882792 - "center": change the shape around its center, default: "ctrl".
27892793 - "data_coordinates": define if data or figure coordinates should be
27902794 used to define the square shape, default: "d"
2795+ - "rotate": Rotate the shape around its center, default: "r".
27912796
27922797 "square" and "center" can be combined. The square shape can be defined
27932798 in data or figure coordinates as determined by the ``data_coordinates``
@@ -2835,8 +2840,6 @@ class RectangleSelector(_SelectorWidget):
28352840 See also: :doc:`/gallery/widgets/rectangle_selector`
28362841 """
28372842
2838- _shape_klass = Rectangle
2839-
28402843 @_api .rename_parameter ("3.5" , "maxdist" , "grab_range" )
28412844 @_api .rename_parameter ("3.5" , "marker_props" , "handle_props" )
28422845 @_api .rename_parameter ("3.5" , "rectprops" , "props" )
@@ -2855,6 +2858,7 @@ def __init__(self, ax, onselect, drawtype='box',
28552858 self ._interactive = interactive
28562859 self .drag_from_anywhere = drag_from_anywhere
28572860 self .ignore_event_outside = ignore_event_outside
2861+ self ._rotation = 0
28582862
28592863 if drawtype == 'none' : # draw a line but make it invisible
28602864 _api .warn_deprecated (
@@ -2872,8 +2876,7 @@ def __init__(self, ax, onselect, drawtype='box',
28722876 props ['animated' ] = self .useblit
28732877 self .visible = props .pop ('visible' , self .visible )
28742878 self ._props = props
2875- to_draw = self ._shape_klass ((0 , 0 ), 0 , 1 , visible = False ,
2876- ** self ._props )
2879+ to_draw = self ._init_shape (** self ._props )
28772880 self .ax .add_patch (to_draw )
28782881 if drawtype == 'line' :
28792882 _api .warn_deprecated (
@@ -2945,6 +2948,10 @@ def _handles_artists(self):
29452948 return (* self ._center_handle .artists , * self ._corner_handles .artists ,
29462949 * self ._edge_handles .artists )
29472950
2951+ def _init_shape (self , ** props ):
2952+ return Rectangle ((0 , 0 ), 0 , 1 , visible = False ,
2953+ rotate_around_center = True , ** props )
2954+
29482955 def _press (self , event ):
29492956 """Button press event handler."""
29502957 # make the drawn box/line visible get the click-coordinates,
@@ -3041,9 +3048,17 @@ def _onmove(self, event):
30413048 refx = event .xdata / (self ._eventpress .xdata + 1e-6 )
30423049 refy = event .ydata / (self ._eventpress .ydata + 1e-6 )
30433050
3051+
3052+ x0 , x1 , y0 , y1 = self ._extents_on_press
30443053 # resize an existing shape
3045- if self ._active_handle and self ._active_handle != 'C' :
3046- x0 , x1 , y0 , y1 = self ._extents_on_press
3054+ if 'rotate' in state and self ._active_handle in self ._corner_order :
3055+ # calculate angle abc
3056+ a = np .array ([self ._eventpress .xdata , self ._eventpress .ydata ])
3057+ b = np .array (self .center )
3058+ c = np .array ([event .xdata , event .ydata ])
3059+ self ._rotation = (np .arctan2 (c [1 ]- b [1 ], c [0 ]- b [0 ]) -
3060+ np .arctan2 (a [1 ]- b [1 ], a [0 ]- b [0 ]))
3061+ elif self ._active_handle and self ._active_handle != 'C' :
30473062 size_on_press = [x1 - x0 , y1 - y0 ]
30483063 center = [x0 + size_on_press [0 ] / 2 , y0 + size_on_press [1 ] / 2 ]
30493064
@@ -3108,6 +3123,7 @@ def _onmove(self, event):
31083123
31093124 # new shape
31103125 else :
3126+ self ._rotation = 0
31113127 # Don't create a new rectangle if there is already one when
31123128 # ignore_event_outside=True
31133129 if self .ignore_event_outside and self ._selection_completed :
@@ -3142,24 +3158,25 @@ def _onmove(self, event):
31423158 @property
31433159 def _rect_bbox (self ):
31443160 if self ._drawtype == 'box' :
3145- x0 = self ._selection_artist .get_x ()
3146- y0 = self ._selection_artist .get_y ()
3147- width = self ._selection_artist .get_width ()
3148- height = self ._selection_artist .get_height ()
3149- return x0 , y0 , width , height
3161+ return self ._selection_artist .get_bbox ().bounds
31503162 else :
31513163 x , y = self ._selection_artist .get_data ()
31523164 x0 , x1 = min (x ), max (x )
31533165 y0 , y1 = min (y ), max (y )
31543166 return x0 , y0 , x1 - x0 , y1 - y0
31553167
3168+ def _get_rotation_transform (self ):
3169+ return Affine2D ().rotate_around (* self .center , self ._rotation )
3170+
31563171 @property
31573172 def corners (self ):
31583173 """Corners of rectangle from lower left, moving clockwise."""
31593174 x0 , y0 , width , height = self ._rect_bbox
31603175 xc = x0 , x0 + width , x0 + width , x0
31613176 yc = y0 , y0 , y0 + height , y0 + height
3162- return xc , yc
3177+ transform = self ._get_rotation_transform ()
3178+ coords = transform .transform (np .array ([xc , yc ]).T ).T
3179+ return coords [0 ], coords [1 ]
31633180
31643181 @property
31653182 def edge_centers (self ):
@@ -3169,7 +3186,9 @@ def edge_centers(self):
31693186 h = height / 2.
31703187 xe = x0 , x0 + w , x0 + width , x0 + w
31713188 ye = y0 + h , y0 , y0 + h , y0 + height
3172- return xe , ye
3189+ transform = self ._get_rotation_transform ()
3190+ coords = transform .transform (np .array ([xe , ye ]).T ).T
3191+ return coords [0 ], coords [1 ]
31733192
31743193 @property
31753194 def center (self ):
@@ -3179,7 +3198,10 @@ def center(self):
31793198
31803199 @property
31813200 def extents (self ):
3182- """Return (xmin, xmax, ymin, ymax)."""
3201+ """
3202+ Return (xmin, xmax, ymin, ymax) as defined by the bounding box before
3203+ rotation.
3204+ """
31833205 x0 , y0 , width , height = self ._rect_bbox
31843206 xmin , xmax = sorted ([x0 , x0 + width ])
31853207 ymin , ymax = sorted ([y0 , y0 + height ])
@@ -3197,6 +3219,17 @@ def extents(self, extents):
31973219 self .set_visible (self .visible )
31983220 self .update ()
31993221
3222+ @property
3223+ def rotation (self ):
3224+ """Rotation in degree."""
3225+ return np .rad2deg (self ._rotation )
3226+
3227+ @rotation .setter
3228+ def rotation (self , value ):
3229+ self ._rotation = np .deg2rad (value )
3230+ # call extents setter to draw shape and update handles positions
3231+ self .extents = self .extents
3232+
32003233 draw_shape = _api .deprecate_privatize_attribute ('3.5' )
32013234
32023235 def _draw_shape (self , extents ):
@@ -3216,6 +3249,7 @@ def _draw_shape(self, extents):
32163249 self ._selection_artist .set_y (ymin )
32173250 self ._selection_artist .set_width (xmax - xmin )
32183251 self ._selection_artist .set_height (ymax - ymin )
3252+ self ._selection_artist .set_angle (self .rotation )
32193253
32203254 elif self ._drawtype == 'line' :
32213255 self ._selection_artist .set_data ([xmin , xmax ], [ymin , ymax ])
@@ -3288,9 +3322,11 @@ class EllipseSelector(RectangleSelector):
32883322 :doc:`/gallery/widgets/rectangle_selector`
32893323 """
32903324
3291- _shape_klass = Ellipse
32923325 draw_shape = _api .deprecate_privatize_attribute ('3.5' )
32933326
3327+ def _init_shape (self , ** props ):
3328+ return Ellipse ((0 , 0 ), 0 , 1 , visible = False , ** props )
3329+
32943330 def _draw_shape (self , extents ):
32953331 x0 , x1 , y0 , y1 = extents
32963332 xmin , xmax = sorted ([x0 , x1 ])
@@ -3303,6 +3339,7 @@ def _draw_shape(self, extents):
33033339 self ._selection_artist .center = center
33043340 self ._selection_artist .width = 2 * a
33053341 self ._selection_artist .height = 2 * b
3342+ self ._selection_artist .set_angle (self .rotation )
33063343 else :
33073344 rad = np .deg2rad (np .arange (31 ) * 12 )
33083345 x = a * np .cos (rad ) + center [0 ]
@@ -3483,7 +3520,8 @@ def __init__(self, ax, onselect, useblit=False,
34833520 move_all = 'shift' , move = 'not-applicable' ,
34843521 square = 'not-applicable' ,
34853522 center = 'not-applicable' ,
3486- data_coordinates = 'not-applicable' )
3523+ data_coordinates = 'not-applicable' ,
3524+ rotate = 'not-applicable' )
34873525 super ().__init__ (ax , onselect , useblit = useblit ,
34883526 state_modifier_keys = state_modifier_keys )
34893527
0 commit comments