Coverage for trimesh/viewer/trackball.py: 92%

106 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-24 04:40 +0000

1# Copied from 

2# https://github.com/mmatl/pyrender/blob/master/pyrender/trackball.py 

3 

4# MIT License 

5# 

6# Copyright (c) 2019 Matthew Matl 

7# 

8# Permission is hereby granted, free of charge, to any person obtaining a copy 

9# of this software and associated documentation files (the "Software"), to deal 

10# in the Software without restriction, including without limitation the rights 

11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 

12# copies of the Software, and to permit persons to whom the Software is 

13# furnished to do so, subject to the following conditions: 

14# 

15# The above copyright notice and this permission notice shall be included in all 

16# copies or substantial portions of the Software. 

17# 

18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 

19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 

21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 

22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 

23# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 

24# SOFTWARE. 

25 

26"""Trackball class for 3D manipulation of viewpoints.""" 

27 

28import numpy as np 

29 

30from .. import transformations 

31 

32 

33class Trackball: 

34 """A trackball class for creating camera transforms from mouse movements.""" 

35 

36 STATE_ROTATE = 0 

37 STATE_PAN = 1 

38 STATE_ROLL = 2 

39 STATE_ZOOM = 3 

40 

41 def __init__(self, pose, size, scale, target=None): 

42 """Initialize a trackball with an initial camera-to-world pose 

43 and the given parameters. 

44 

45 Parameters 

46 ---------- 

47 pose : [4,4] 

48 An initial camera-to-world pose for the trackball. 

49 

50 size : (float, float) 

51 The width and height of the camera image in pixels. 

52 

53 scale : float 

54 The diagonal of the scene's bounding box -- 

55 used for ensuring translation motions are sufficiently 

56 fast for differently-sized scenes. 

57 

58 target : (3,) float 

59 The center of the scene in world coordinates. 

60 The trackball will revolve around this point. 

61 """ 

62 self._size = np.array(size) 

63 self._scale = float(scale) 

64 

65 self._pose = pose 

66 self._n_pose = pose 

67 

68 if target is None: 

69 self._target = np.array([0.0, 0.0, 0.0]) 

70 self._n_target = np.array([0.0, 0.0, 0.0]) 

71 else: 

72 self._target = target 

73 self._n_target = target 

74 

75 self._state = Trackball.STATE_ROTATE 

76 

77 @property 

78 def pose(self): 

79 """autolab_core.RigidTransform : The current camera-to-world pose.""" 

80 return self._n_pose 

81 

82 def set_state(self, state): 

83 """Set the state of the trackball in order to change the effect of 

84 dragging motions. 

85 

86 Parameters 

87 ---------- 

88 state : int 

89 One of Trackball.STATE_ROTATE, Trackball.STATE_PAN, 

90 Trackball.STATE_ROLL, and Trackball.STATE_ZOOM. 

91 """ 

92 self._state = state 

93 

94 def resize(self, size): 

95 """Resize the window. 

96 

97 Parameters 

98 ---------- 

99 size : (float, float) 

100 The new width and height of the camera image in pixels. 

101 """ 

102 self._size = np.array(size) 

103 

104 def down(self, point): 

105 """Record an initial mouse press at a given point. 

106 

107 Parameters 

108 ---------- 

109 point : (2,) int 

110 The x and y pixel coordinates of the mouse press. 

111 """ 

112 self._pdown = np.array(point, dtype=np.float32) 

113 self._pose = self._n_pose 

114 self._target = self._n_target 

115 

116 def drag(self, point): 

117 """Update the tracball during a drag. 

118 

119 Parameters 

120 ---------- 

121 point : (2,) int 

122 The current x and y pixel coordinates of the mouse during a drag. 

123 This will compute a movement for the trackball with the relative 

124 motion between this point and the one marked by down(). 

125 """ 

126 point = np.array(point, dtype=np.float32) 

127 # get the "down" point defaulting to current point making 

128 # this a no-op if the "down" event didn't trigger for some reason 

129 dx, dy = point - getattr(self, "_pdown", point) 

130 mindim = 0.3 * np.min(self._size) 

131 

132 target = self._target 

133 x_axis = self._pose[:3, 0].flatten() 

134 y_axis = self._pose[:3, 1].flatten() 

135 z_axis = self._pose[:3, 2].flatten() 

136 eye = self._pose[:3, 3].flatten() 

137 

138 # Interpret drag as a rotation 

139 if self._state == Trackball.STATE_ROTATE: 

140 x_angle = -dx / mindim 

141 x_rot_mat = transformations.rotation_matrix(x_angle, y_axis, target) 

142 

143 y_angle = dy / mindim 

144 y_rot_mat = transformations.rotation_matrix(y_angle, x_axis, target) 

145 

146 self._n_pose = y_rot_mat.dot(x_rot_mat.dot(self._pose)) 

147 

148 # Interpret drag as a roll about the camera axis 

149 elif self._state == Trackball.STATE_ROLL: 

150 center = self._size / 2.0 

151 v_init = self._pdown - center 

152 v_curr = point - center 

153 v_init = v_init / np.linalg.norm(v_init) 

154 v_curr = v_curr / np.linalg.norm(v_curr) 

155 

156 theta = -np.arctan2(v_curr[1], v_curr[0]) + np.arctan2(v_init[1], v_init[0]) 

157 

158 rot_mat = transformations.rotation_matrix(theta, z_axis, target) 

159 

160 self._n_pose = rot_mat.dot(self._pose) 

161 

162 # Interpret drag as a camera pan in view plane 

163 elif self._state == Trackball.STATE_PAN: 

164 dx = -dx / (5.0 * mindim) * self._scale 

165 dy = -dy / (5.0 * mindim) * self._scale 

166 

167 translation = dx * x_axis + dy * y_axis 

168 self._n_target = self._target + translation 

169 t_tf = np.eye(4) 

170 t_tf[:3, 3] = translation 

171 self._n_pose = t_tf.dot(self._pose) 

172 

173 # Interpret drag as a zoom motion 

174 elif self._state == Trackball.STATE_ZOOM: 

175 radius = np.linalg.norm(eye - target) 

176 ratio = 0.0 

177 if dy > 0: 

178 ratio = np.exp(abs(dy) / (0.5 * self._size[1])) - 1.0 

179 elif dy < 0: 

180 ratio = 1.0 - np.exp(dy / (0.5 * (self._size[1]))) 

181 translation = -np.sign(dy) * ratio * radius * z_axis 

182 t_tf = np.eye(4) 

183 t_tf[:3, 3] = translation 

184 self._n_pose = t_tf.dot(self._pose) 

185 

186 def scroll(self, clicks): 

187 """Zoom using a mouse scroll wheel motion. 

188 

189 Parameters 

190 ---------- 

191 clicks : int 

192 The number of clicks. Positive numbers indicate forward wheel 

193 movement. 

194 """ 

195 target = self._target 

196 ratio = 0.90 

197 

198 mult = 1.0 

199 if clicks > 0: 

200 mult = ratio**clicks 

201 elif clicks < 0: 

202 mult = (1.0 / ratio) ** abs(clicks) 

203 

204 z_axis = self._n_pose[:3, 2].flatten() 

205 eye = self._n_pose[:3, 3].flatten() 

206 radius = np.linalg.norm(eye - target) 

207 translation = (mult * radius - radius) * z_axis 

208 t_tf = np.eye(4) 

209 t_tf[:3, 3] = translation 

210 self._n_pose = t_tf.dot(self._n_pose) 

211 

212 z_axis = self._pose[:3, 2].flatten() 

213 eye = self._pose[:3, 3].flatten() 

214 radius = np.linalg.norm(eye - target) 

215 translation = (mult * radius - radius) * z_axis 

216 t_tf = np.eye(4) 

217 t_tf[:3, 3] = translation 

218 self._pose = t_tf.dot(self._pose) 

219 

220 def rotate(self, azimuth, axis=None): 

221 """Rotate the trackball about the "Up" axis by azimuth radians. 

222 

223 Parameters 

224 ---------- 

225 azimuth : float 

226 The number of radians to rotate. 

227 """ 

228 target = self._target 

229 

230 y_axis = self._n_pose[:3, 1].flatten() 

231 if axis is not None: 

232 y_axis = axis 

233 x_rot_mat = transformations.rotation_matrix(azimuth, y_axis, target) 

234 self._n_pose = x_rot_mat.dot(self._n_pose) 

235 

236 y_axis = self._pose[:3, 1].flatten() 

237 if axis is not None: 

238 y_axis = axis 

239 x_rot_mat = transformations.rotation_matrix(azimuth, y_axis, target) 

240 self._pose = x_rot_mat.dot(self._pose)