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

Skip to content

Commit 89701eb

Browse files
committed
Soften the arcball edge using a 'border' parameter
- revise _arcball() to soften the edge, according to border width parameter - drop _arcball() 'style' function parameter - add trackballborder rcParam - _arcball() 'border' case: normalize, so result vector is on unit sphere - math.sin/cos/sqrt -> np.sin/cos/sqrt: use numpy instead of math for consistency - remove 'Bell' style - rename arcball -> sphere, Shoemake -> arcball - update documentation - update test
1 parent a293e31 commit 89701eb

File tree

6 files changed

+81
-75
lines changed

6 files changed

+81
-75
lines changed

doc/api/toolkits/mplot3d/view_angles.rst

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -70,23 +70,20 @@ move the mouse in circles with a handedness opposite to the desired rotation,
7070
counterintuitively.
7171

7272
A different variety of trackball rotates along the shortest arc on the virtual
73-
sphere (``mouserotationstyle: arcball``); it is a variation on Ken Shoemake's
74-
ARCBALL [Shoemake1992]_. Rotating around the viewing direction is straightforward
75-
with it (grab the ball near its edge instead of near the center).
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.
7675

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

86-
Shoemake's arcball has an abrupt edge; this is remedied in Gavin Bell's arcball
87-
(``mouserotationstyle: Bell``), originally written for OpenGL [Bell1988]_. It is used
88-
in Blender and Meshlab.
89-
9087
Henriksen et al. [Henriksen2002]_ provide an overview. In summary:
9188

9289
.. list-table::
@@ -111,19 +108,13 @@ Henriksen et al. [Henriksen2002]_ provide an overview. In summary:
111108
- ✔️
112109
- ❌
113110
- ✔️
114-
* - arcball
111+
* - sphere
115112
- ❌
116113
- ✔️
117114
- ✔️
118115
- ❌
119116
- ✔️
120-
* - Shoemake
121-
- ❌
122-
- ✔️
123-
- ✔️
124-
- ✔️
125-
- ❌
126-
* - Bell
117+
* - arcball
127118
- ❌
128119
- ✔️
129120
- ✔️
@@ -134,16 +125,16 @@ Henriksen et al. [Henriksen2002]_ provide an overview. In summary:
134125
.. [1] The way it was prior to v3.10; this is also MATLAB's style
135126
.. [2] Mouse controls roll too (not only azimuth and elevation)
136127
.. [3] Figure reacts the same way to mouse movements, regardless of orientation (no difference between 'poles' and 'equator')
137-
.. [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)
128+
.. [4] Returning mouse to original position returns figure to original orientation (rotation is independent of the details of the path the mouse took)
138129
.. [5] The style has a corresponding natural implementation as a mechanical device
139-
.. [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
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)
140131
141132
You can try out one of the various mouse rotation styles using::
142133

143134
.. code::
144135
145136
import matplotlib as mpl
146-
mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'arcball', 'Shoemake', or 'Bell'
137+
mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'sphere', or 'arcball'
147138
148139
import numpy as np
149140
import matplotlib.pyplot as plt
@@ -169,22 +160,37 @@ Alternatively, create a file ``matplotlibrc``, with contents::
169160
(or any of the other styles, instead of ``arcball``), and then run any of
170161
the :ref:`mplot3d-examples-index` examples.
171162

172-
The size of the virtual trackball or arcball can be adjusted as well,
163+
The size of the virtual trackball, sphere, or arcball can be adjusted
173164
by setting :rc:`axes3d.trackballsize`. This specifies how much
174165
mouse motion is needed to obtain a given rotation angle (when near the center),
175-
and it controls where the edge of the arcball is (how far from the center,
176-
how close to the plot edge).
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).
177168
The size is specified in units of the Axes bounding box,
178-
i.e., to make the trackball span the whole bounding box, set it to 1.
169+
i.e., to make the arcball span the whole bounding box, set it to 1.
179170
A size of about 2/3 appears to work reasonably well; this is the default.
180171

181-
----
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 :rc:`axes3d.trackballsize`/2 + :rc:`axes3d.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+
$\sqrt 2 \approx 1.414$.
188+
182189

183190
.. [Shoemake1992] Ken Shoemake, "ARCBALL: A user interface for specifying
184191
three-dimensional rotation using a mouse", in Proceedings of Graphics
185192
Interface '92, 1992, pp. 151-156, https://doi.org/10.20380/GI1992.18
186193
187-
188194
.. [Bell1988] Gavin Bell, in the examples included with the GLUT (OpenGL
189195
Utility Toolkit) library,
190196
https://github.com/markkilgard/glut/blob/master/progs/examples/trackball.h

doc/users/next_whats_new/mouse_rotation.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ To try out one of the various mouse rotation styles:
2020
.. code::
2121
2222
import matplotlib as mpl
23-
mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'arcball', 'Shoemake', or 'Bell'
23+
mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'sphere', or 'arcball'
2424
2525
import numpy as np
2626
import matplotlib.pyplot as plt

lib/matplotlib/mpl-data/matplotlibrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,9 +433,10 @@
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, arcball, Shoemake, Bell}
436+
#axes3d.mouserotationstyle: arcball # {azel, trackball, sphere, arcball}
437437
# See also https://matplotlib.org/stable/api/toolkits/mplot3d/view_angles.html#rotation-with-mouse
438438
#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)
439440

440441
## ***************************************************************************
441442
## * AXIS *

lib/matplotlib/rcsetup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,9 +1132,9 @@ def _convert_validator_spec(key, conv):
11321132
"axes3d.yaxis.panecolor": validate_color, # 3d background pane
11331133
"axes3d.zaxis.panecolor": validate_color, # 3d background pane
11341134

1135-
"axes3d.mouserotationstyle": ["azel", "trackball", "arcball",
1136-
"Shoemake", "Bell"],
1135+
"axes3d.mouserotationstyle": ["azel", "trackball", "sphere", "arcball"],
11371136
"axes3d.trackballsize": validate_float,
1137+
"axes3d.trackballborder": validate_float,
11381138

11391139
# scatter props
11401140
"scatter.marker": _validate_marker,

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1508,31 +1508,37 @@ def _calc_coord(self, xv, yv, renderer=None):
15081508
p2 = p1 - scale*vec
15091509
return p2, pane_idx
15101510

1511-
def _arcball(self, x: float, y: float, style: str) -> np.ndarray:
1511+
def _arcball(self, x: float, y: float) -> np.ndarray:
15121512
"""
15131513
Convert a point (x, y) to a point on a virtual trackball.
15141514
1515-
This is either Ken Shoemake's arcball (a sphere) or
1516-
Gavin Bell's (a sphere combined with a hyperbola).
1515+
This is Ken Shoemake's arcball (a sphere), modified
1516+
to soften the abrupt edge (optionally).
15171517
See: Ken Shoemake, "ARCBALL: A user interface for specifying
15181518
three-dimensional rotation using a mouse." in
15191519
Proceedings of Graphics Interface '92, 1992, pp. 151-156,
15201520
https://doi.org/10.20380/GI1992.18
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.
15211524
"""
15221525
s = mpl.rcParams['axes3d.trackballsize'] / 2
1526+
b = mpl.rcParams['axes3d.trackballborder'] / s
15231527
x /= s
15241528
y /= s
15251529
r2 = x*x + y*y
1526-
if style == 'Bell':
1527-
if r2 > 0.5:
1528-
p = np.array([1/(2*math.sqrt(r2)), x, y])/math.sqrt(1/(4*r2)+r2)
1529-
else:
1530-
p = np.array([math.sqrt(1-r2), x, y])
1531-
else: # 'arcball', 'Shoemake'
1532-
if r2 > 1:
1533-
p = np.array([0, x/math.sqrt(r2), y/math.sqrt(r2)])
1534-
else:
1535-
p = np.array([math.sqrt(1-r2), x, y])
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)
1540+
else:
1541+
p = np.array([0, x/r, y/r])
15361542
return p
15371543

15381544
def _on_move(self, event):
@@ -1587,12 +1593,12 @@ def _on_move(self, event):
15871593
nk = np.linalg.norm(k)
15881594
th = nk / mpl.rcParams['axes3d.trackballsize']
15891595
dq = _Quaternion(np.cos(th), k*np.sin(th)/nk)
1590-
else: # 'arcball', 'Shoemake', 'Bell'
1591-
current_vec = self._arcball(self._sx/w, self._sy/h, style)
1592-
new_vec = self._arcball(x/w, y/h, style)
1593-
if style == 'arcball':
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':
15941600
dq = _Quaternion.rotate_from_to(current_vec, new_vec)
1595-
else: # 'Shoemake', 'Bell'
1601+
else: # 'arcball'
15961602
dq = _Quaternion(0, new_vec) * _Quaternion(0, -current_vec)
15971603

15981604
q = dq * q
@@ -4017,7 +4023,7 @@ def rotate_from_to(cls, r1, r2):
40174023
else:
40184024
q = cls(1, [0, 0, 0]) # = 1, no rotation
40194025
else:
4020-
q = cls(math.cos(th), k*math.sin(th)/nk)
4026+
q = cls(np.cos(th), k*np.sin(th)/nk)
40214027
return q
40224028

40234029
@classmethod

lib/mpl_toolkits/mplot3d/tests/test_axes3d.py

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,14 +1944,15 @@ def test_quaternion():
19441944

19451945

19461946
@pytest.mark.parametrize('style',
1947-
('azel', 'trackball', 'arcball', 'Shoemake', 'Bell'))
1947+
('azel', 'trackball', 'sphere', 'arcball'))
19481948
def test_rotate(style):
19491949
"""Test rotating using the left mouse button."""
19501950
if style == 'azel':
19511951
s = 0.5
19521952
else:
19531953
s = mpl.rcParams['axes3d.trackballsize'] / 2
19541954
s *= 0.5
1955+
mpl.rcParams['axes3d.trackballborder'] = 0
19551956
with mpl.rc_context({'axes3d.mouserotationstyle': style}):
19561957
for roll, dx, dy in [
19571958
[0, 1, 0],
@@ -1992,29 +1993,21 @@ def test_rotate(style):
19921993
('trackball', 30, 0, 1): (-24.531578, -15.277726, 33.340403),
19931994
('trackball', 30, 0.5, c): (-13.869588, -25.319385, 33.129920),
19941995

1995-
('arcball', 0, 1, 0): (0, -30, 0),
1996-
('arcball', 0, 0, 1): (-30, 0, 0),
1997-
('arcball', 0, 0.5, c): (-25.658906, -16.102114, 3.690068),
1998-
('arcball', 0, 2, 0): (0, -90, 0),
1999-
('arcball', 30, 1, 0): (14.477512, -26.565051, 26.565051),
2000-
('arcball', 30, 0, 1): (-25.658906, -16.102114, 33.690068),
2001-
('arcball', 30, 0.5, c): (-14.477512, -26.565051, 33.434949),
2002-
2003-
('Shoemake', 0, 1, 0): (0, -60, 0),
2004-
('Shoemake', 0, 0, 1): (-60, 0, 0),
2005-
('Shoemake', 0, 0.5, c): (-48.590378, -40.893395, 19.106605),
2006-
('Shoemake', 0, 2, 0): (0, 180, 0),
2007-
('Shoemake', 30, 1, 0): (25.658906, -56.309932, 16.102114),
2008-
('Shoemake', 30, 0, 1): (-48.590378, -40.893395, 49.106605),
2009-
('Shoemake', 30, 0.5, c): (-25.658906, -56.309932, 43.897886),
2010-
2011-
('Bell', 0, 1, 0): (0, -60, 0),
2012-
('Bell', 0, 0, 1): (-60, 0, 0),
2013-
('Bell', 0, 0.5, c): (-48.590378, -40.893395, 19.106605),
2014-
('Bell', 0, 2, 0): (0, -126.869898, 0),
2015-
('Bell', 30, 1, 0): (25.658906, -56.309932, 16.102114),
2016-
('Bell', 30, 0, 1): (-48.590378, -40.893395, 49.106605),
2017-
('Bell', 30, 0.5, c): (-25.658906, -56.309932, 43.897886)}
1996+
('sphere', 0, 1, 0): (0, -30, 0),
1997+
('sphere', 0, 0, 1): (-30, 0, 0),
1998+
('sphere', 0, 0.5, c): (-25.658906, -16.102114, 3.690068),
1999+
('sphere', 0, 2, 0): (0, -90, 0),
2000+
('sphere', 30, 1, 0): (14.477512, -26.565051, 26.565051),
2001+
('sphere', 30, 0, 1): (-25.658906, -16.102114, 33.690068),
2002+
('sphere', 30, 0.5, c): (-14.477512, -26.565051, 33.434949),
2003+
2004+
('arcball', 0, 1, 0): (0, -60, 0),
2005+
('arcball', 0, 0, 1): (-60, 0, 0),
2006+
('arcball', 0, 0.5, c): (-48.590378, -40.893395, 19.106605),
2007+
('arcball', 0, 2, 0): (0, 180, 0),
2008+
('arcball', 30, 1, 0): (25.658906, -56.309932, 16.102114),
2009+
('arcball', 30, 0, 1): (-48.590378, -40.893395, 49.106605),
2010+
('arcball', 30, 0.5, c): (-25.658906, -56.309932, 43.897886)}
20182011
new_elev, new_azim, new_roll = expectations[(style, roll, dx, dy)]
20192012
np.testing.assert_allclose((ax.elev, ax.azim, ax.roll),
20202013
(new_elev, new_azim, new_roll), atol=1e-6)

0 commit comments

Comments
 (0)