Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implement review suggestions
  • Loading branch information
MischaMegens2 committed Sep 27, 2024
commit 133c916b83a0354c5a2279c698110c981217295e
99 changes: 68 additions & 31 deletions doc/api/toolkits/mplot3d/view_angles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ further documented in the `.mplot3d.axes3d.Axes3D.view_init` API.
:align: center


.. _toolkit_mouse-rotation:

Rotation with mouse
===================

Expand All @@ -48,99 +50,132 @@ There are various ways to accomplish this; the style of mouse rotation
can be specified by setting ``rcParams.axes3d.mouserotationstyle``, see
:doc:`/users/explain/customizing`.

Originally (with ``mouserotationstyle: azel``), the 2D mouse position
corresponded directly to azimuth and elevation; this is also how it is done
Originally (prior to v3.10), the 2D mouse position corresponded directly
Comment thread
MischaMegens2 marked this conversation as resolved.
Resolved
to azimuth and elevation; this is also how it is done
in `MATLAB <https://www.mathworks.com/help/matlab/ref/view.html>`_.
To keep it this way, set ``mouserotationstyle: azel``.
This approach works fine for polar plots, where the *z* axis is special;
Comment thread
scottshambaugh marked this conversation as resolved.
Resolved
however, it leads to a kind of 'gimbal lock' when looking down the *z* axis:
the plot reacts differently to mouse movement, dependent on the particular
orientation at hand. Also, 'roll' cannot be controlled.

As an alternative, there are various mouse rotation styles where the mouse
manipulates a 'trackball'. In its simplest form (``mouserotationstyle: trackball``),
manipulates a virtual 'trackball'. In its simplest form (``mouserotationstyle: trackball``),
the trackball rotates around an in-plane axis perpendicular to the mouse motion
(it is as if there is a plate laying on the trackball; the plate itself is fixed
in orientation, but you can drag the plate with the mouse, thus rotating the ball).
This is more natural to work with than the ``azel`` style; however,
the plot cannot be easily rotated around the viewing direction - one has to
drag the mouse in circles with a handedness opposite to the desired rotation.
move the mouse in circles with a handedness opposite to the desired rotation,
counterintuitively.

A different variety of trackball rotates along the shortest arc on the virtual
sphere (``mouserotationstyle: arcball``); it is a variation on Ken Shoemake's
ARCBALL [Shoemake1992]_. Rotating around the viewing direction is straightforward
with it. Shoemake's original arcball is also available
(``mouserotationstyle: Shoemake``); it is free of hysteresis, i.e.,
returning mouse to the original position returns the figure to its original
orientation, the rotation is independent of the details of the path the mouse
took. However, Shoemake's arcball rotates at twice the angular rate of the
mouse movement (it is quite noticeable, especially when adjusting roll).
with it (grab the ball near its edge instead of near the center).

Shoemake's original arcball is also available (``mouserotationstyle: Shoemake``);
it is free of hysteresis, i.e., returning mouse to the original position
returns the figure to its original orientation, the rotation is independent
of the details of the path the mouse took, which could be desirable.
However, Shoemake's arcball rotates at twice the angular rate of the
mouse movement (it is quite noticeable, especially when adjusting roll),
and it lacks an obvious mechanical equivalent; arguably, the path-independent rotation is unnatural.
So it is a trade-off.

Shoemake's arcball has an abrupt edge; this is remedied in Holroyd's arcball
(``mouserotationstyle: Holroyd``).

Henriksen et al. [Henriksen2002]_ provide an overview.

In summary:
Henriksen et al. [Henriksen2002]_ provide an overview. In summary:

.. list-table::
:width: 100%
:widths: 30 20 20 20 35
:widths: 30 20 20 20 20 35

* - Style
- traditional [1]_
- incl. roll [2]_
- uniform [3]_
- path independent [4]_
- mechanical counterpart [5]_
* - azel
- ✔️
- ❌
- ❌
- ✔️
- ✔️
* - trackball
- ❌
- ~
- ✓ [6]_
- ✔️
- ❌
- ✔️
* - arcball
- ❌
- ✔️
- ✔️
- ❌
- ✔️
* - Shoemake
- ❌
- ✔️
- ✔️
- ✔️
- ❌
* - Holroyd
- ❌
- ✔️
- ✔️
- ✔️
- ❌


.. [1] The way it was historically; this is also MATLAB's style
.. [1] The way it was prior to v3.10; this is also MATLAB's style
.. [2] Mouse controls roll too (not only azimuth and elevation)
.. [3] Figure reacts the same way to mouse movements, regardless of orientation (no difference between 'poles' and 'equator')
.. [4] Returning mouse to original position returns figure to original orientation (no hysteresis: rotation is independent of the details of the path the mouse took)
.. [5] The style has a corresponding natural implementation as a mechanical device
.. [6] While it is possible to control roll with the ``trackball`` style, this is not very intuitive (it requires moving the mouse in large circles) and the resulting roll is in the opposite direction

Try it out by adding a file ``matplotlibrc`` to folder ``matplotlib\galleries\examples\mplot3d``,
with contents::
You can try out one of the various mouse rotation styles using::

axes3d.mouserotationstyle: arcball
.. code::

import matplotlib as mpl
mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'arcball', 'Shoemake', or 'Holroyd'

(or any of the other styles), and run a suitable example, e.g.::
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm

ax = plt.figure().add_subplot(projection='3d')

X = np.arange(-5, 5, 0.25)
Y = np.arange(-5, 5, 0.25)
X, Y = np.meshgrid(X, Y)
R = np.sqrt(X**2 + Y**2)
Z = np.sin(R)

surf = ax.plot_surface(X, Y, Z, cmap=cm.coolwarm,
linewidth=0, antialiased=False)

python surfaced3d.py
plt.show()

(If eternal compatibility with the horrors of the past is less of a consideration
for you, then it is likely that you would want to go with ``arcball``, ``Shoemake``,
or ``Holroyd``.)
Alternatively, create a file ``matplotlibrc``, with contents::

The size of the trackball or arcball can be adjusted by setting
``rcParams.axes3d.trackballsize``, in units of the Axes bounding box;
axes3d.mouserotationstyle: arcball

(or any of the other styles, instead of ``arcball``), and then run any of
the :ref:`mplot3d-examples-index` examples.

The size of the virtual trackball or arcball can be adjusted as well,
by setting ``rcParams.axes3d.trackballsize``. This specifies how much
Comment thread
MischaMegens2 marked this conversation as resolved.
Resolved
mouse motion is needed to obtain a given rotation angle (when near the center),
and it controls where the edge of the arcball is (how far from the center,
how close to the plot edge).
The size is specified in units of the Axes bounding box,
i.e., to make the trackball span the whole bounding box, set it to 1.
A size of ca. 2/3 appears to work reasonably well.
A size of about 2/3 appears to work reasonably well; this is the default.

----

Expand All @@ -149,8 +184,10 @@ A size of ca. 2/3 appears to work reasonably well.
Interface '92, 1992, pp. 151-156, https://doi.org/10.20380/GI1992.18

.. [Henriksen2002] Knud Henriksen, Jon Sporring, Kasper Hornbæk,
"Virtual Trackballs Revisited", in Proceedings of DSAGM'2002:
http://www.diku.dk/~kash/papers/DSAGM2002_henriksen.pdf;
and in IEEE Transactions on Visualization
and Computer Graphics, Volume 10, Issue 2, March-April 2004, pp. 206-216,
"Virtual Trackballs Revisited", in Proceedings of DSAGM'2002
`[pdf]`__;
and in IEEE Transactions on Visualization and Computer Graphics,
Volume 10, Issue 2, March-April 2004, pp. 206-216,
https://doi.org/10.1109/TVCG.2004.1260772

__ https://web.archive.org/web/20240607102518/http://hjemmesider.diku.dk/~kash/papers/DSAGM2002_henriksen.pdf
32 changes: 31 additions & 1 deletion doc/users/next_whats_new/mouse_rotation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,37 @@ degrees of freedom (azimuth, elevation, and roll). By default,
it uses a variation on Ken Shoemake's ARCBALL [1]_.
Comment thread
scottshambaugh marked this conversation as resolved.
Resolved
The particular style of mouse rotation can be set via
``rcParams.axes3d.mouserotationstyle``.
Comment thread
MischaMegens2 marked this conversation as resolved.
Resolved
See also :doc:`/api/toolkits/mplot3d/view_angles`.
See also :ref:`toolkit_mouse-rotation`.

To revert to the original mouse rotation style,
create a file ``matplotlibrc`` with contents::

axes3d.mouserotationstyle: azel

To try out one of the various mouse rotation styles:

.. code::

import matplotlib as mpl
mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'arcball', 'Shoemake', or 'Holroyd'

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm

ax = plt.figure().add_subplot(projection='3d')

X = np.arange(-5, 5, 0.25)
Y = np.arange(-5, 5, 0.25)
X, Y = np.meshgrid(X, Y)
R = np.sqrt(X**2 + Y**2)
Z = np.sin(R)

surf = ax.plot_surface(X, Y, Z, cmap=cm.coolwarm,
linewidth=0, antialiased=False)

plt.show()


.. [1] Ken Shoemake, "ARCBALL: A user interface for specifying
three-dimensional rotation using a mouse", in Proceedings of Graphics
Expand Down
49 changes: 18 additions & 31 deletions lib/mpl_toolkits/mplot3d/axes3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -1508,10 +1508,11 @@ def _calc_coord(self, xv, yv, renderer=None):
p2 = p1 - scale*vec
return p2, pane_idx

def _arcball(self, x: float, y: float, Holroyd: bool) -> np.ndarray:
def _arcball(self, x: float, y: float, style: str) -> np.ndarray:
"""
Convert a point (x, y) to a point on a virtual trackball
This is Ken Shoemake's arcball
either Ken Shoemake's arcball (a sphere) or
Comment thread
scottshambaugh marked this conversation as resolved.
Resolved
Tom Holroyd's (a sphere combined with a hyperbola).
See: Ken Shoemake, "ARCBALL: A user interface for specifying
three-dimensional rotation using a mouse." in
Proceedings of Graphics Interface '92, 1992, pp. 151-156,
Expand All @@ -1521,12 +1522,12 @@ def _arcball(self, x: float, y: float, Holroyd: bool) -> np.ndarray:
x /= s
y /= s
r2 = x*x + y*y
if Holroyd:
if style == 'Holroyd':
if r2 > 0.5:
p = np.array([1/(2*math.sqrt(r2)), x, y])/math.sqrt(1/(4*r2)+r2)
else:
p = np.array([math.sqrt(1-r2), x, y])
else: # Shoemake
else: # 'arcball', 'Shoemake'
if r2 > 1:
p = np.array([0, x/math.sqrt(r2), y/math.sqrt(r2)])
else:
Expand Down Expand Up @@ -1577,38 +1578,24 @@ def _on_move(self, event):
azim = self.azim + dazim
roll = self.roll
else:
# Convert to quaternion
elev = np.deg2rad(self.elev)
azim = np.deg2rad(self.azim)
roll = np.deg2rad(self.roll)
q = _Quaternion.from_cardan_angles(elev, azim, roll)
q = _Quaternion.from_cardan_angles(
*np.deg2rad((self.elev, self.azim, self.roll)))

if style in ['arcball', 'Shoemake', 'Holroyd']:
# Update quaternion
is_Holroyd = (style == 'Holroyd')
current_vec = self._arcball(self._sx/w, self._sy/h, is_Holroyd)
new_vec = self._arcball(x/w, y/h, is_Holroyd)
if style == 'trackball':
k = np.array([0, -dy/h, dx/w])
nk = np.linalg.norm(k)
th = nk / mpl.rcParams['axes3d.trackballsize']
dq = _Quaternion(np.cos(th), k*np.sin(th)/nk)
else: # 'arcball', 'Shoemake', 'Holroyd'
current_vec = self._arcball(self._sx/w, self._sy/h, style)
new_vec = self._arcball(x/w, y/h, style)
if style == 'arcball':
dq = _Quaternion.rotate_from_to(current_vec, new_vec)
else: # 'Shoemake', 'Holroyd'
dq = _Quaternion(0, new_vec) * _Quaternion(0, -current_vec)
q = dq * q
elif style == 'trackball':
s = mpl.rcParams['axes3d.trackballsize'] / 2
k = np.array([0, -(y-self._sy)/h, (x-self._sx)/w]) / s
nk = np.linalg.norm(k)
th = nk / 2
dq = _Quaternion(math.cos(th), k*math.sin(th)/nk)
q = dq * q
else:
warnings.warn("Mouse rotation style (axes3d.mouserotationstyle: " +
style + ") not recognized.")

# Convert to elev, azim, roll
elev, azim, roll = q.as_cardan_angles()
elev = np.rad2deg(elev)
azim = np.rad2deg(azim)
roll = np.rad2deg(roll)

q = dq * q
elev, azim, roll = np.rad2deg(q.as_cardan_angles())

# update view
vertical_axis = self._axis_names[self._vertical_axis]
Expand Down