Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 77cbe08

Browse files
Merge pull request #28841 from MischaMegens2/mplot3d-mouse-rotation-style
Make mplot3d mouse rotation style adjustable
2 parents 9f8eeba + bcffb92 commit 77cbe08

File tree

6 files changed

+337
-61
lines changed

6 files changed

+337
-61
lines changed

doc/api/toolkits/mplot3d/view_angles.rst

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,168 @@ further documented in the `.mplot3d.axes3d.Axes3D.view_init` API.
3838

3939
.. plot:: gallery/mplot3d/view_planes_3d.py
4040
:align: center
41+
42+
43+
.. _toolkit_mouse-rotation:
44+
45+
Rotation with mouse
46+
===================
47+
48+
3D plots can be reoriented by dragging the mouse.
49+
There are various ways to accomplish this; the style of mouse rotation
50+
can be specified by setting :rc:`axes3d.mouserotationstyle`, see
51+
:doc:`/users/explain/customizing`.
52+
53+
Prior to v3.10, the 2D mouse position corresponded directly
54+
to azimuth and elevation; this is also how it is done
55+
in `MATLAB <https://www.mathworks.com/help/matlab/ref/view.html>`_.
56+
To keep it this way, set ``mouserotationstyle: azel``.
57+
This approach works fine for spherical coordinate plots, where the *z* axis is special;
58+
however, it leads to a kind of 'gimbal lock' when looking down the *z* axis:
59+
the plot reacts differently to mouse movement, dependent on the particular
60+
orientation at hand. Also, 'roll' cannot be controlled.
61+
62+
As an alternative, there are various mouse rotation styles where the mouse
63+
manipulates a virtual 'trackball'. In its simplest form (``mouserotationstyle: trackball``),
64+
the trackball rotates around an in-plane axis perpendicular to the mouse motion
65+
(it is as if there is a plate laying on the trackball; the plate itself is fixed
66+
in orientation, but you can drag the plate with the mouse, thus rotating the ball).
67+
This is more natural to work with than the ``azel`` style; however,
68+
the plot cannot be easily rotated around the viewing direction - one has to
69+
move the mouse in circles with a handedness opposite to the desired rotation,
70+
counterintuitively.
71+
72+
A different variety of trackball rotates along the shortest arc on the virtual
73+
sphere (``mouserotationstyle: sphere``). Rotating around the viewing direction
74+
is straightforward with it: grab the ball near its edge instead of near the center.
75+
76+
Ken Shoemake's ARCBALL [Shoemake1992]_ is also available (``mouserotationstyle: Shoemake``);
77+
it resembles the ``sphere`` style, but is free of hysteresis,
78+
i.e., returning mouse to the original position
79+
returns the figure to its original orientation; the rotation is independent
80+
of the details of the path the mouse took, which could be desirable.
81+
However, Shoemake's arcball rotates at twice the angular rate of the
82+
mouse movement (it is quite noticeable, especially when adjusting roll),
83+
and it lacks an obvious mechanical equivalent; arguably, the path-independent
84+
rotation is not natural (however convenient), it could take some getting used to.
85+
So it is a trade-off.
86+
87+
Henriksen et al. [Henriksen2002]_ provide an overview. In summary:
88+
89+
.. list-table::
90+
:width: 100%
91+
:widths: 30 20 20 20 20 35
92+
93+
* - Style
94+
- traditional [1]_
95+
- incl. roll [2]_
96+
- uniform [3]_
97+
- path independent [4]_
98+
- mechanical counterpart [5]_
99+
* - azel
100+
- ✔️
101+
- ❌
102+
- ❌
103+
- ✔️
104+
- ✔️
105+
* - trackball
106+
- ❌
107+
- ✓ [6]_
108+
- ✔️
109+
- ❌
110+
- ✔️
111+
* - sphere
112+
- ❌
113+
- ✔️
114+
- ✔️
115+
- ❌
116+
- ✔️
117+
* - arcball
118+
- ❌
119+
- ✔️
120+
- ✔️
121+
- ✔️
122+
- ❌
123+
124+
125+
.. [1] The way it was prior to v3.10; this is also MATLAB's style
126+
.. [2] Mouse controls roll too (not only azimuth and elevation)
127+
.. [3] Figure reacts the same way to mouse movements, regardless of orientation (no difference between 'poles' and 'equator')
128+
.. [4] Returning mouse to original position returns figure to original orientation (rotation is independent of the details of the path the mouse took)
129+
.. [5] The style has a corresponding natural implementation as a mechanical device
130+
.. [6] While it is possible to control roll with the ``trackball`` style, this is not immediately obvious (it requires moving the mouse in large circles) and a bit counterintuitive (the resulting roll is in the opposite direction)
131+
132+
You can try out one of the various mouse rotation styles using:
133+
134+
.. code::
135+
136+
import matplotlib as mpl
137+
mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'sphere', or 'arcball'
138+
139+
import numpy as np
140+
import matplotlib.pyplot as plt
141+
from matplotlib import cm
142+
143+
ax = plt.figure().add_subplot(projection='3d')
144+
145+
X = np.arange(-5, 5, 0.25)
146+
Y = np.arange(-5, 5, 0.25)
147+
X, Y = np.meshgrid(X, Y)
148+
R = np.sqrt(X**2 + Y**2)
149+
Z = np.sin(R)
150+
151+
surf = ax.plot_surface(X, Y, Z, cmap=cm.coolwarm,
152+
linewidth=0, antialiased=False)
153+
154+
plt.show()
155+
156+
Alternatively, create a file ``matplotlibrc``, with contents::
157+
158+
axes3d.mouserotationstyle: trackball
159+
160+
(or any of the other styles, instead of ``trackball``), and then run any of
161+
the :ref:`mplot3d-examples-index` examples.
162+
163+
The size of the virtual trackball, sphere, or arcball can be adjusted
164+
by setting :rc:`axes3d.trackballsize`. This specifies how much
165+
mouse motion is needed to obtain a given rotation angle (when near the center),
166+
and it controls where the edge of the sphere or arcball is (how far from
167+
the center, hence how close to the plot edge).
168+
The size is specified in units of the Axes bounding box,
169+
i.e., to make the arcball span the whole bounding box, set it to 1.
170+
A size of about 2/3 appears to work reasonably well; this is the default.
171+
172+
Both arcballs (``mouserotationstyle: sphere`` and
173+
``mouserotationstyle: arcball``) have a noticeable edge; the edge can be made
174+
less abrupt by specifying a border width, :rc:`axes3d.trackballborder`.
175+
This works somewhat like Gavin Bell's arcball, which was
176+
originally written for OpenGL [Bell1988]_, and is used in Blender and Meshlab.
177+
Bell's arcball extends the arcball's spherical control surface with a hyperbola;
178+
the two are smoothly joined. However, the hyperbola extends all the way beyond
179+
the edge of the plot. In the mplot3d sphere and arcball style, the border extends
180+
to a radius ``trackballsize/2 + trackballborder``.
181+
Beyond the border, the style works like the original: it controls roll only.
182+
A border width of about 0.2 appears to work well; this is the default.
183+
To obtain the original Shoemake's arcball with a sharp border,
184+
set the border width to 0.
185+
For an extended border similar to Bell's arcball, where the transition from
186+
the arcball to the border occurs at 45°, set the border width to
187+
:math:`\sqrt 2 \approx 1.414`.
188+
The border is a circular arc, wrapped around the arcball sphere cylindrically
189+
(like a doughnut), joined smoothly to the sphere, much like Bell's hyperbola.
190+
191+
192+
.. [Shoemake1992] Ken Shoemake, "ARCBALL: A user interface for specifying
193+
three-dimensional rotation using a mouse", in Proceedings of Graphics
194+
Interface '92, 1992, pp. 151-156, https://doi.org/10.20380/GI1992.18
195+
196+
.. [Bell1988] Gavin Bell, in the examples included with the GLUT (OpenGL
197+
Utility Toolkit) library,
198+
https://github.com/markkilgard/glut/blob/master/progs/examples/trackball.h
199+
200+
.. [Henriksen2002] Knud Henriksen, Jon Sporring, Kasper Hornbæk,
201+
"Virtual Trackballs Revisited", in IEEE Transactions on Visualization
202+
and Computer Graphics, Volume 10, Issue 2, March-April 2004, pp. 206-216,
203+
https://doi.org/10.1109/TVCG.2004.1260772 `[full-text]`__;
204+
205+
__ https://www.researchgate.net/publication/8329656_Virtual_Trackballs_Revisited#fullTextFileContent

doc/users/next_whats_new/mouse_rotation.rst

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,42 @@ Rotating 3d plots with the mouse
44
Rotating three-dimensional plots with the mouse has been made more intuitive.
55
The plot now reacts the same way to mouse movement, independent of the
66
particular orientation at hand; and it is possible to control all 3 rotational
7-
degrees of freedom (azimuth, elevation, and roll). It uses a variation on
8-
Ken Shoemake's ARCBALL [Shoemake1992]_.
7+
degrees of freedom (azimuth, elevation, and roll). By default,
8+
it uses a variation on Ken Shoemake's ARCBALL [1]_.
9+
The particular style of mouse rotation can be set via
10+
:rc:`axes3d.mouserotationstyle`.
11+
See also :ref:`toolkit_mouse-rotation`.
912

10-
.. [Shoemake1992] Ken Shoemake, "ARCBALL: A user interface for specifying
11-
three-dimensional rotation using a mouse." in Proceedings of Graphics
13+
To revert to the original mouse rotation style,
14+
create a file ``matplotlibrc`` with contents::
15+
16+
axes3d.mouserotationstyle: azel
17+
18+
To try out one of the various mouse rotation styles:
19+
20+
.. code::
21+
22+
import matplotlib as mpl
23+
mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'sphere', or 'arcball'
24+
25+
import numpy as np
26+
import matplotlib.pyplot as plt
27+
from matplotlib import cm
28+
29+
ax = plt.figure().add_subplot(projection='3d')
30+
31+
X = np.arange(-5, 5, 0.25)
32+
Y = np.arange(-5, 5, 0.25)
33+
X, Y = np.meshgrid(X, Y)
34+
R = np.sqrt(X**2 + Y**2)
35+
Z = np.sin(R)
36+
37+
surf = ax.plot_surface(X, Y, Z, cmap=cm.coolwarm,
38+
linewidth=0, antialiased=False)
39+
40+
plt.show()
41+
42+
43+
.. [1] Ken Shoemake, "ARCBALL: A user interface for specifying
44+
three-dimensional rotation using a mouse", in Proceedings of Graphics
1245
Interface '92, 1992, pp. 151-156, https://doi.org/10.20380/GI1992.18

lib/matplotlib/mpl-data/matplotlibrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,11 @@
433433
#axes3d.yaxis.panecolor: (0.90, 0.90, 0.90, 0.5) # background pane on 3D axes
434434
#axes3d.zaxis.panecolor: (0.925, 0.925, 0.925, 0.5) # background pane on 3D axes
435435

436+
#axes3d.mouserotationstyle: arcball # {azel, trackball, sphere, arcball}
437+
# See also https://matplotlib.org/stable/api/toolkits/mplot3d/view_angles.html#rotation-with-mouse
438+
#axes3d.trackballsize: 0.667 # trackball diameter, in units of the Axes bbox
439+
#axes3d.trackballborder: 0.2 # trackball border width, in units of the Axes bbox (only for 'sphere' and 'arcball' style)
440+
436441
## ***************************************************************************
437442
## * AXIS *
438443
## ***************************************************************************

lib/matplotlib/rcsetup.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,10 @@ def _convert_validator_spec(key, conv):
11191119
"axes3d.yaxis.panecolor": validate_color, # 3d background pane
11201120
"axes3d.zaxis.panecolor": validate_color, # 3d background pane
11211121

1122+
"axes3d.mouserotationstyle": ["azel", "trackball", "sphere", "arcball"],
1123+
"axes3d.trackballsize": validate_float,
1124+
"axes3d.trackballborder": validate_float,
1125+
11221126
# scatter props
11231127
"scatter.marker": _validate_marker,
11241128
"scatter.edgecolors": validate_string,

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,20 +1510,35 @@ def _calc_coord(self, xv, yv, renderer=None):
15101510

15111511
def _arcball(self, x: float, y: float) -> np.ndarray:
15121512
"""
1513-
Convert a point (x, y) to a point on a virtual trackball
1514-
This is Ken Shoemake's arcball
1513+
Convert a point (x, y) to a point on a virtual trackball.
1514+
1515+
This is Ken Shoemake's arcball (a sphere), modified
1516+
to soften the abrupt edge (optionally).
15151517
See: Ken Shoemake, "ARCBALL: A user interface for specifying
15161518
three-dimensional rotation using a mouse." in
15171519
Proceedings of Graphics Interface '92, 1992, pp. 151-156,
15181520
https://doi.org/10.20380/GI1992.18
1519-
"""
1520-
x *= 2
1521-
y *= 2
1521+
The smoothing of the edge is inspired by Gavin Bell's arcball
1522+
(a sphere combined with a hyperbola), but here, the sphere
1523+
is combined with a section of a cylinder, so it has finite support.
1524+
"""
1525+
s = mpl.rcParams['axes3d.trackballsize'] / 2
1526+
b = mpl.rcParams['axes3d.trackballborder'] / s
1527+
x /= s
1528+
y /= s
15221529
r2 = x*x + y*y
1523-
if r2 > 1:
1524-
p = np.array([0, x/math.sqrt(r2), y/math.sqrt(r2)])
1530+
r = np.sqrt(r2)
1531+
ra = 1 + b
1532+
a = b * (1 + b/2)
1533+
ri = 2/(ra + 1/ra)
1534+
if r < ri:
1535+
p = np.array([np.sqrt(1 - r2), x, y])
1536+
elif r < ra:
1537+
dr = ra - r
1538+
p = np.array([a - np.sqrt((a + dr) * (a - dr)), x, y])
1539+
p /= np.linalg.norm(p)
15251540
else:
1526-
p = np.array([math.sqrt(1-r2), x, y])
1541+
p = np.array([0, x/r, y/r])
15271542
return p
15281543

15291544
def _on_move(self, event):
@@ -1561,23 +1576,35 @@ def _on_move(self, event):
15611576
if dx == 0 and dy == 0:
15621577
return
15631578

1564-
# Convert to quaternion
1565-
elev = np.deg2rad(self.elev)
1566-
azim = np.deg2rad(self.azim)
1567-
roll = np.deg2rad(self.roll)
1568-
q = _Quaternion.from_cardan_angles(elev, azim, roll)
1569-
1570-
# Update quaternion - a variation on Ken Shoemake's ARCBALL
1571-
current_vec = self._arcball(self._sx/w, self._sy/h)
1572-
new_vec = self._arcball(x/w, y/h)
1573-
dq = _Quaternion.rotate_from_to(current_vec, new_vec)
1574-
q = dq * q
1575-
1576-
# Convert to elev, azim, roll
1577-
elev, azim, roll = q.as_cardan_angles()
1578-
azim = np.rad2deg(azim)
1579-
elev = np.rad2deg(elev)
1580-
roll = np.rad2deg(roll)
1579+
style = mpl.rcParams['axes3d.mouserotationstyle']
1580+
if style == 'azel':
1581+
roll = np.deg2rad(self.roll)
1582+
delev = -(dy/h)*180*np.cos(roll) + (dx/w)*180*np.sin(roll)
1583+
dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll)
1584+
elev = self.elev + delev
1585+
azim = self.azim + dazim
1586+
roll = self.roll
1587+
else:
1588+
q = _Quaternion.from_cardan_angles(
1589+
*np.deg2rad((self.elev, self.azim, self.roll)))
1590+
1591+
if style == 'trackball':
1592+
k = np.array([0, -dy/h, dx/w])
1593+
nk = np.linalg.norm(k)
1594+
th = nk / mpl.rcParams['axes3d.trackballsize']
1595+
dq = _Quaternion(np.cos(th), k*np.sin(th)/nk)
1596+
else: # 'sphere', 'arcball'
1597+
current_vec = self._arcball(self._sx/w, self._sy/h)
1598+
new_vec = self._arcball(x/w, y/h)
1599+
if style == 'sphere':
1600+
dq = _Quaternion.rotate_from_to(current_vec, new_vec)
1601+
else: # 'arcball'
1602+
dq = _Quaternion(0, new_vec) * _Quaternion(0, -current_vec)
1603+
1604+
q = dq * q
1605+
elev, azim, roll = np.rad2deg(q.as_cardan_angles())
1606+
1607+
# update view
15811608
vertical_axis = self._axis_names[self._vertical_axis]
15821609
self.view_init(
15831610
elev=elev,
@@ -4088,7 +4115,7 @@ def rotate_from_to(cls, r1, r2):
40884115
k = np.cross(r1, r2)
40894116
nk = np.linalg.norm(k)
40904117
th = np.arctan2(nk, np.dot(r1, r2))
4091-
th = th/2
4118+
th /= 2
40924119
if nk == 0: # r1 and r2 are parallel or anti-parallel
40934120
if np.dot(r1, r2) < 0:
40944121
warnings.warn("Rotation defined by anti-parallel vectors is ambiguous")
@@ -4100,7 +4127,7 @@ def rotate_from_to(cls, r1, r2):
41004127
else:
41014128
q = cls(1, [0, 0, 0]) # = 1, no rotation
41024129
else:
4103-
q = cls(math.cos(th), k*math.sin(th)/nk)
4130+
q = cls(np.cos(th), k*np.sin(th)/nk)
41044131
return q
41054132

41064133
@classmethod
@@ -4125,10 +4152,11 @@ def as_cardan_angles(self):
41254152
"""
41264153
The inverse of `from_cardan_angles()`.
41274154
Note that the angles returned are in radians, not degrees.
4155+
The angles are not sensitive to the quaternion's norm().
41284156
"""
41294157
qw = self.scalar
41304158
qx, qy, qz = self.vector[..., :]
41314159
azim = np.arctan2(2*(-qw*qz+qx*qy), qw*qw+qx*qx-qy*qy-qz*qz)
4132-
elev = np.arcsin( 2*( qw*qy+qz*qx)/(qw*qw+qx*qx+qy*qy+qz*qz)) # noqa E201
4133-
roll = np.arctan2(2*( qw*qx-qy*qz), qw*qw-qx*qx-qy*qy+qz*qz) # noqa E201
4160+
elev = np.arcsin(np.clip(2*(qw*qy+qz*qx)/(qw*qw+qx*qx+qy*qy+qz*qz), -1, 1))
4161+
roll = np.arctan2(2*(qw*qx-qy*qz), qw*qw-qx*qx-qy*qy+qz*qz)
41344162
return elev, azim, roll

0 commit comments

Comments
 (0)