Repository: pyvista/pyvista

Issue:
Incorrect rotation with `rotate_vector` with 90° angles
### Describe the bug, what's wrong, and what you expected.

The `rotate_vector` function does not produce the expected rotation for some vectors with an angle of 90° or -90°.
Instead the result is a distorted shape as shown in the screenshot.
This issue emerged after the 0.42 release. It appears that the problem was introduced in PR #4674 specifically in the code added to the `axis_angle_rotation` function within `pyvista/core/utilities/transformations.py`:
```python
# the cos and sin functions can introduce some numerical error
# round the elements to exact values for special cases where we know
# sin/cos should evaluate exactly to 0 or 1
if angle % (np.pi / 2) == 0:
    R = np.round(R)
```

From my analysis, the transformation matrix generated by `axis_angle_rotation(axis=(1, 1, 0), angle=90)` is as follows: 
```python
[[ 1.  0.  1.  0.]
 [ 0.  1. -1.  0.]
 [-1.  1.  0.  0.]
 [ 0.  0.  0.  1.]]
```
However it should be :
```python
[[ 0.5       0.5       sqrt(2)/2  0.]
 [ 0.5       0.5       -sqrt(2)/2 0.]
 [-sqrt(2)/2 sqrt(2)/2 0.         0.]
 [ 0.        0.        0.         1.]]
```

### Steps to reproduce the bug.

```python
import pyvista as pv

cylinder = pv.Cylinder()
cylinder = cylinder.rotate_vector(vector=(1, 1, 0), angle=90)
cylinder.plot()
```

### System Information

```shell
--------------------------------------------------------------------------------
  Date: Mon Nov 06 18:16:27 2023 Romance Standard Time       

                OS : Windows
            CPU(s) : 32
           Machine : AMD64
      Architecture : 64bit
               RAM : 63.7 GiB
       Environment : Python
        GPU Vendor : NVIDIA Corporation
      GPU Renderer : NVIDIA GeForce RTX 4090/PCIe/SSE2       
       GPU Version : 4.5.0 NVIDIA 536.40
  MathText Support : False

  Python 3.9.18 | packaged by conda-forge | (main, Aug 30 2023, 03:40:31) [MSC
  v.1929 64 bit (AMD64)]

           pyvista : 0.42.3
               vtk : 9.2.6
             numpy : 1.25.2
        matplotlib : 3.7.2
            scooby : 0.7.2
             pooch : v1.7.0
            pillow : 10.0.0
           IPython : 8.14.0
        ipywidgets : 7.8.0
             scipy : 1.11.2
              tqdm : 4.66.1
            meshio : 5.3.4
        jupyterlab : 3.4.8
      nest_asyncio : 1.5.7
--------------------------------------------------------------------------------
```


### Screenshots

![image](https://github.com/pyvista/pyvista/assets/22714778/3c5641e7-96ff-43a6-abb0-2aa60fd64486)


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pyvista/pyvista/pull/5181?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) Report
> Merging [#5181](https://app.codecov.io/gh/pyvista/pyvista/pull/5181?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) (8d72b99) into [main](https://app.codecov.io/gh/pyvista/pyvista/commit/341c2a5bf7e2faa4a72be63e39c64c7e5e8703b1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) (341c2a5) will **decrease** coverage by `3.11%`.
> Report is 20 commits behind head on main.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##             main    #5181      +/-   ##
==========================================
- Coverage   96.09%   92.98%   -3.11%     
==========================================
  Files         131      131              
  Lines       21872    21868       -4     
==========================================
- Hits        21017    20335     -682     
- Misses        855     1533     +678     
```


Patch:
diff --git a/pyvista/core/utilities/transformations.py b/pyvista/core/utilities/transformations.py
index 88bc7fb0..e842e895 100644
--- a/pyvista/core/utilities/transformations.py
+++ b/pyvista/core/utilities/transformations.py
@@ -112,13 +112,17 @@ def axis_angle_rotation(axis, angle, point=None, deg=True):
     K = np.zeros((3, 3))
     K[[2, 0, 1], [1, 2, 0]] = axis
     K += -K.T
-    R = np.eye(3) + np.sin(angle) * K + (1 - np.cos(angle)) * K @ K
 
     # the cos and sin functions can introduce some numerical error
     # round the elements to exact values for special cases where we know
     # sin/cos should evaluate exactly to 0 or 1
+    sin_angle = np.sin(angle)
+    cos_angle = np.cos(angle)
     if angle % (np.pi / 2) == 0:
-        R = np.round(R)
+        cos_angle = round(cos_angle)
+        sin_angle = round(sin_angle)
+
+    R = np.eye(3) + sin_angle * K + (1 - cos_angle) * K @ K
 
     augmented = np.eye(4)
     augmented[:-1, :-1] = R


Test patch:
diff --git a/tests/core/test_dataset.py b/tests/core/test_dataset.py
index 667db1af..e92f4733 100644
--- a/tests/core/test_dataset.py
+++ b/tests/core/test_dataset.py
@@ -339,6 +339,28 @@ def test_rotate_should_match_vtk_rotation(angle, axis, grid):
     assert np.allclose(grid_a.points, grid_b.points, equal_nan=True)
 
 
+def test_rotate_90_degrees_four_times_should_return_original_geometry():
+    sphere = pv.Sphere()
+    sphere.rotate_y(90, inplace=True)
+    sphere.rotate_y(90, inplace=True)
+    sphere.rotate_y(90, inplace=True)
+    sphere.rotate_y(90, inplace=True)
+    assert np.all(sphere.points == pv.Sphere().points)
+
+
+def test_rotate_180_degrees_two_times_should_return_original_geometry():
+    sphere = pv.Sphere()
+    sphere.rotate_x(180, inplace=True)
+    sphere.rotate_x(180, inplace=True)
+    assert np.all(sphere.points == pv.Sphere().points)
+
+
+def test_rotate_vector_90_degrees_should_not_distort_geometry():
+    cylinder = pv.Cylinder()
+    rotated = cylinder.rotate_vector(vector=(1, 1, 0), angle=90)
+    assert np.isclose(cylinder.volume, rotated.volume)
+
+
 def test_make_points_double(grid):
     grid.points = grid.points.astype(np.float32)
     assert grid.points.dtype == np.float32