diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 5d522cd0988a..10f693ace0cf 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -1568,10 +1568,11 @@ def _on_move(self, event): q = _Quaternion.from_cardan_angles(elev, azim, roll) # Update quaternion - a variation on Ken Shoemake's ARCBALL - current_vec = self._arcball(self._sx/w, self._sy/h) - new_vec = self._arcball(x/w, y/h) + scale = np.sqrt(2)/2 # slow down the rate of rotation + current_vec = self._arcball(self._sx*scale/w, self._sy*scale/h) + new_vec = self._arcball(x*scale/w, y*scale/h) dq = _Quaternion.rotate_from_to(current_vec, new_vec) - q = dq * q + q = dq * dq * q # Convert to elev, azim, roll elev, azim, roll = q.as_cardan_angles() @@ -4020,11 +4021,14 @@ def from_cardan_angles(cls, elev, azim, roll): def as_cardan_angles(self): """ The inverse of `from_cardan_angles()`. + This function acts on the quaternion as if it were unit normed. Note that the angles returned are in radians, not degrees. """ qw = self.scalar qx, qy, qz = self.vector[..., :] azim = np.arctan2(2*(-qw*qz+qx*qy), qw*qw+qx*qx-qy*qy-qz*qz) - elev = np.arcsin( 2*( qw*qy+qz*qx)/(qw*qw+qx*qx+qy*qy+qz*qz)) # noqa E201 + # Clip below is to avoid floating point round-off errors + elev = np.arcsin(np.clip(2*(qw*qy+qz*qx) + / (qw*qw+qx*qx+qy*qy+qz*qz), -1, 1)) roll = np.arctan2(2*( qw*qx-qy*qz), qw*qw-qx*qx-qy*qy+qz*qz) # noqa E201 return elev, azim, roll diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 0afcae99c980..07ecb0086030 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -1950,10 +1950,10 @@ def test_rotate(): for roll, dx, dy, new_elev, new_azim, new_roll in [ [0, 0.5, 0, 0, -90, 0], [30, 0.5, 0, 30, -90, 0], - [0, 0, 0.5, -90, 0, 0], + [0, 0, 0.5, -90, -180, 180], [30, 0, 0.5, -60, -90, 90], - [0, 0.5, 0.5, -45, -90, 45], - [30, 0.5, 0.5, -15, -90, 45]]: + [0, np.sqrt(2)/4, np.sqrt(2)/4, -45, -90, 45], + [30, np.sqrt(2)/4, np.sqrt(2)/4, -15, -90, 45]]: fig = plt.figure() ax = fig.add_subplot(1, 1, 1, projection='3d') ax.view_init(0, 0, roll) @@ -1967,9 +1967,8 @@ def test_rotate(): xdata=dx*ax._pseudo_w, ydata=dy*ax._pseudo_h)) fig.canvas.draw() - assert np.isclose(ax.elev, new_elev) - assert np.isclose(ax.azim, new_azim) - assert np.isclose(ax.roll, new_roll) + np.testing.assert_allclose((ax.elev, ax.azim, ax.roll), + (new_elev, new_azim, new_roll), atol=1e-6) def test_pan():