Repository: matplotlib/matplotlib

Issue:
[Bug]: axes3d.py's _on_move() converts the roll angle to radians, but then passes it to view_init() as if it were still in degrees
### Bug summary

In \lib\mpl_toolkits\mplot3d\axes3d.py, `_on_move()` deals with rotation of 3d axes using the mouse.  In line 1522, the roll angle (in degrees) is converted to radians:
`    roll = np.deg2rad(self.roll)`
This roll in radians is used to calculate a new elevation and azimuth, like so:
```
    delev = -(dy/h)*180*np.cos(roll) + (dx/w)*180*np.sin(roll)
    dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll)
    elev = self.elev + delev
    azim = self.azim + dazim
```
A moment later, the view is updated:
```
    self.view_init(
        elev=elev,
        azim=azim,
        roll=roll,
        vertical_axis=vertical_axis,
        share=True,
    )
```
However, `view_init()` expects its parameters to be in degrees, not radians. As a consequence, the roll now diminishes by a factor pi/180 with every mouse movement. Not intended.


### Code for reproduction

```Python
# Run the surface3d.py example, adding
ax.roll = 45
# It shows the plot, in the intended funny orientation (roll=45)
# Then move the mouse - you will see the orientation jump suddenly (to roll=0)
```


### Actual outcome

The figure orientation has jumped to roll=0, after trying to rotate it only slightly by dragging the mouse:
![Figure_rotated](https://github.com/matplotlib/matplotlib/assets/122418839/ccbc7f47-dc27-499c-bee4-296b5b5164cb)


### Expected outcome

The figure is close to its original orientation (before dragging the mouse), at roll=45:
![Figure_1](https://github.com/matplotlib/matplotlib/assets/122418839/6e572bc9-e1f4-4a87-87f3-4b571ef06288)


### Additional information

Fix: add a line:
`roll = self.roll`
right after updating `elev` and `azim` (i.e., after line 1526).


### Operating system

All, presumably; but I noticed it on Windows

### Matplotlib Version

3.10.0.dev191+ge5af947d1b.d20240517

### Matplotlib Backend

tkagg

### Python version

Python 3.12.3

### Jupyter version

_No response_

### Installation

pip


Discussion:

Patch:
diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py
index d0f5c8d2b23b..677c2668d4e9 100644
--- a/lib/mpl_toolkits/mplot3d/axes3d.py
+++ b/lib/mpl_toolkits/mplot3d/axes3d.py
@@ -1524,6 +1524,7 @@ def _on_move(self, event):
             dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll)
             elev = self.elev + delev
             azim = self.azim + dazim
+            roll = self.roll
             vertical_axis = self._axis_names[self._vertical_axis]
             self.view_init(
                 elev=elev,


Test patch:
diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
index ed56e5505d8e..c339e35e903c 100644
--- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
+++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
@@ -1766,6 +1766,31 @@ def test_shared_axes_retick():
     assert ax2.get_zlim() == (-0.5, 2.5)
 
 
+def test_rotate():
+    """Test rotating using the left mouse button."""
+    for roll in [0, 30]:
+        fig = plt.figure()
+        ax = fig.add_subplot(1, 1, 1, projection='3d')
+        ax.view_init(0, 0, roll)
+        ax.figure.canvas.draw()
+
+        # drag mouse horizontally to change azimuth
+        dx = 0.1
+        dy = 0.2
+        ax._button_press(
+            mock_event(ax, button=MouseButton.LEFT, xdata=0, ydata=0))
+        ax._on_move(
+            mock_event(ax, button=MouseButton.LEFT,
+                           xdata=dx*ax._pseudo_w, ydata=dy*ax._pseudo_h))
+        ax.figure.canvas.draw()
+        roll_radians = np.deg2rad(ax.roll)
+        cs = np.cos(roll_radians)
+        sn = np.sin(roll_radians)
+        assert ax.elev == (-dy*180*cs + dx*180*sn)
+        assert ax.azim == (-dy*180*sn - dx*180*cs)
+        assert ax.roll == roll
+
+
 def test_pan():
     """Test mouse panning using the middle mouse button."""