hub/venv/lib/python3.7/site-packages/trimesh/viewer/trackball.py

242 lines
7.9 KiB
Python
Raw Normal View History

# Copied from
# https://github.com/mmatl/pyrender/blob/master/pyrender/trackball.py
# MIT License
#
# Copyright (c) 2019 Matthew Matl
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Trackball class for 3D manipulation of viewpoints.
"""
import numpy as np
from .. import transformations
class Trackball(object):
"""A trackball class for creating camera transforms from mouse movements.
"""
STATE_ROTATE = 0
STATE_PAN = 1
STATE_ROLL = 2
STATE_ZOOM = 3
def __init__(self, pose, size, scale,
target=np.array([0.0, 0.0, 0.0])):
"""Initialize a trackball with an initial camera-to-world pose
and the given parameters.
Parameters
----------
pose : [4,4]
An initial camera-to-world pose for the trackball.
size : (float, float)
The width and height of the camera image in pixels.
scale : float
The diagonal of the scene's bounding box --
used for ensuring translation motions are sufficiently
fast for differently-sized scenes.
target : (3,) float
The center of the scene in world coordinates.
The trackball will revolve around this point.
"""
self._size = np.array(size)
self._scale = float(scale)
self._pose = pose
self._n_pose = pose
self._target = target
self._n_target = target
self._state = Trackball.STATE_ROTATE
@property
def pose(self):
"""autolab_core.RigidTransform : The current camera-to-world pose.
"""
return self._n_pose
def set_state(self, state):
"""Set the state of the trackball in order to change the effect of
dragging motions.
Parameters
----------
state : int
One of Trackball.STATE_ROTATE, Trackball.STATE_PAN,
Trackball.STATE_ROLL, and Trackball.STATE_ZOOM.
"""
self._state = state
def resize(self, size):
"""Resize the window.
Parameters
----------
size : (float, float)
The new width and height of the camera image in pixels.
"""
self._size = np.array(size)
def down(self, point):
"""Record an initial mouse press at a given point.
Parameters
----------
point : (2,) int
The x and y pixel coordinates of the mouse press.
"""
self._pdown = np.array(point, dtype=np.float32)
self._pose = self._n_pose
self._target = self._n_target
def drag(self, point):
"""Update the tracball during a drag.
Parameters
----------
point : (2,) int
The current x and y pixel coordinates of the mouse during a drag.
This will compute a movement for the trackball with the relative
motion between this point and the one marked by down().
"""
point = np.array(point, dtype=np.float32)
dx, dy = point - self._pdown
mindim = 0.3 * np.min(self._size)
target = self._target
x_axis = self._pose[:3, 0].flatten()
y_axis = self._pose[:3, 1].flatten()
z_axis = self._pose[:3, 2].flatten()
eye = self._pose[:3, 3].flatten()
# Interpret drag as a rotation
if self._state == Trackball.STATE_ROTATE:
x_angle = -dx / mindim
x_rot_mat = transformations.rotation_matrix(
x_angle, y_axis, target
)
y_angle = dy / mindim
y_rot_mat = transformations.rotation_matrix(
y_angle, x_axis, target
)
self._n_pose = y_rot_mat.dot(x_rot_mat.dot(self._pose))
# Interpret drag as a roll about the camera axis
elif self._state == Trackball.STATE_ROLL:
center = self._size / 2.0
v_init = self._pdown - center
v_curr = point - center
v_init = v_init / np.linalg.norm(v_init)
v_curr = v_curr / np.linalg.norm(v_curr)
theta = (-np.arctan2(v_curr[1], v_curr[0]) +
np.arctan2(v_init[1], v_init[0]))
rot_mat = transformations.rotation_matrix(theta, z_axis, target)
self._n_pose = rot_mat.dot(self._pose)
# Interpret drag as a camera pan in view plane
elif self._state == Trackball.STATE_PAN:
dx = -dx / (5.0 * mindim) * self._scale
dy = -dy / (5.0 * mindim) * self._scale
translation = dx * x_axis + dy * y_axis
self._n_target = self._target + translation
t_tf = np.eye(4)
t_tf[:3, 3] = translation
self._n_pose = t_tf.dot(self._pose)
# Interpret drag as a zoom motion
elif self._state == Trackball.STATE_ZOOM:
radius = np.linalg.norm(eye - target)
ratio = 0.0
if dy > 0:
ratio = np.exp(abs(dy) / (0.5 * self._size[1])) - 1.0
elif dy < 0:
ratio = 1.0 - np.exp(dy / (0.5 * (self._size[1])))
translation = -np.sign(dy) * ratio * radius * z_axis
t_tf = np.eye(4)
t_tf[:3, 3] = translation
self._n_pose = t_tf.dot(self._pose)
def scroll(self, clicks):
"""Zoom using a mouse scroll wheel motion.
Parameters
----------
clicks : int
The number of clicks. Positive numbers indicate forward wheel
movement.
"""
target = self._target
ratio = 0.90
mult = 1.0
if clicks > 0:
mult = ratio**clicks
elif clicks < 0:
mult = (1.0 / ratio)**abs(clicks)
z_axis = self._n_pose[:3, 2].flatten()
eye = self._n_pose[:3, 3].flatten()
radius = np.linalg.norm(eye - target)
translation = (mult * radius - radius) * z_axis
t_tf = np.eye(4)
t_tf[:3, 3] = translation
self._n_pose = t_tf.dot(self._n_pose)
z_axis = self._pose[:3, 2].flatten()
eye = self._pose[:3, 3].flatten()
radius = np.linalg.norm(eye - target)
translation = (mult * radius - radius) * z_axis
t_tf = np.eye(4)
t_tf[:3, 3] = translation
self._pose = t_tf.dot(self._pose)
def rotate(self, azimuth, axis=None):
"""Rotate the trackball about the "Up" axis by azimuth radians.
Parameters
----------
azimuth : float
The number of radians to rotate.
"""
target = self._target
y_axis = self._n_pose[:3, 1].flatten()
if axis is not None:
y_axis = axis
x_rot_mat = transformations.rotation_matrix(azimuth, y_axis, target)
self._n_pose = x_rot_mat.dot(self._n_pose)
y_axis = self._pose[:3, 1].flatten()
if axis is not None:
y_axis = axis
x_rot_mat = transformations.rotation_matrix(azimuth, y_axis, target)
self._pose = x_rot_mat.dot(self._pose)