@@ -164,6 +164,7 @@ def __init__(
164
164
# Enable drawing of axes by Axes3D class
165
165
self .set_axis_on ()
166
166
self .M = None
167
+ self .invM = None
167
168
168
169
# func used to format z -- fall back on major formatters
169
170
self .fmt_zdata = None
@@ -455,6 +456,7 @@ def draw(self, renderer):
455
456
456
457
# add the projection matrix to the renderer
457
458
self .M = self .get_proj ()
459
+ self .invM = np .linalg .inv (self .M )
458
460
459
461
collections_and_patches = (
460
462
artist for artist in self ._children
@@ -1060,45 +1062,97 @@ def format_zdata(self, z):
1060
1062
val = func (z )
1061
1063
return val
1062
1064
1063
- def format_coord (self , xd , yd ):
1065
+ def format_coord (self , xv , yv , renderer = None ):
1064
1066
"""
1065
- Given the 2D view coordinates attempt to guess a 3D coordinate.
1066
- Looks for the nearest edge to the point and then assumes that
1067
- the point is at the same z location as the nearest point on the edge .
1067
+ Return a string giving the current view rotation angles, or the x, y, z
1068
+ coordinates of the point on the nearest axis pane underneath the mouse
1069
+ cursor, depending on the mouse button pressed .
1068
1070
"""
1069
-
1070
- if self .M is None :
1071
- return ''
1071
+ coords = ''
1072
1072
1073
1073
if self .button_pressed in self ._rotate_btn :
1074
- # ignore xd and yd and display angles instead
1075
- norm_elev = art3d ._norm_angle (self .elev )
1076
- norm_azim = art3d ._norm_angle (self .azim )
1077
- norm_roll = art3d ._norm_angle (self .roll )
1078
- return (f"elevation={ norm_elev :.0f} \N{DEGREE SIGN} , "
1079
- f"azimuth={ norm_azim :.0f} \N{DEGREE SIGN} , "
1080
- f"roll={ norm_roll :.0f} \N{DEGREE SIGN} "
1081
- ).replace ("-" , "\N{MINUS SIGN} " )
1082
-
1083
- # nearest edge
1084
- p0 , p1 = min (self ._tunit_edges (),
1085
- key = lambda edge : proj3d ._line2d_seg_dist (
1086
- (xd , yd ), edge [0 ][:2 ], edge [1 ][:2 ]))
1087
-
1088
- # scale the z value to match
1089
- x0 , y0 , z0 = p0
1090
- x1 , y1 , z1 = p1
1091
- d0 = np .hypot (x0 - xd , y0 - yd )
1092
- d1 = np .hypot (x1 - xd , y1 - yd )
1093
- dt = d0 + d1
1094
- z = d1 / dt * z0 + d0 / dt * z1
1095
-
1096
- x , y , z = proj3d .inv_transform (xd , yd , z , self .M )
1097
-
1098
- xs = self .format_xdata (x )
1099
- ys = self .format_ydata (y )
1100
- zs = self .format_zdata (z )
1101
- return f'x={ xs } , y={ ys } , z={ zs } '
1074
+ # ignore xv and yv and display angles instead
1075
+ coords = self ._rotation_coords ()
1076
+
1077
+ elif self .M is not None :
1078
+ coords = self ._location_coords (xv , yv , renderer )
1079
+
1080
+ return coords
1081
+
1082
+ def _rotation_coords (self ):
1083
+ """
1084
+ Return the rotation angles as a string.
1085
+ """
1086
+ norm_elev = art3d ._norm_angle (self .elev )
1087
+ norm_azim = art3d ._norm_angle (self .azim )
1088
+ norm_roll = art3d ._norm_angle (self .roll )
1089
+ coords = (f"elevation={ norm_elev :.0f} \N{DEGREE SIGN} , "
1090
+ f"azimuth={ norm_azim :.0f} \N{DEGREE SIGN} , "
1091
+ f"roll={ norm_roll :.0f} \N{DEGREE SIGN} "
1092
+ ).replace ("-" , "\N{MINUS SIGN} " )
1093
+ return coords
1094
+
1095
+ def _location_coords (self , xv , yv , renderer ):
1096
+ """
1097
+ Return the location on the axis pane underneath the cursor as a string.
1098
+ """
1099
+ p1 = self ._calc_coord (xv , yv , renderer )
1100
+ xs = self .format_xdata (p1 [0 ])
1101
+ ys = self .format_ydata (p1 [1 ])
1102
+ zs = self .format_zdata (p1 [2 ])
1103
+ coords = f'x={ xs } , y={ ys } , z={ zs } '
1104
+ return coords
1105
+
1106
+ def _get_camera_loc (self ):
1107
+ """
1108
+ Returns the current camera location in data coordinates.
1109
+ """
1110
+ cx , cy , cz , dx , dy , dz = self ._get_w_centers_ranges ()
1111
+ c = np .array ([cx , cy , cz ])
1112
+ r = np .array ([dx , dy , dz ])
1113
+
1114
+ if self ._focal_length == np .inf : # orthographic projection
1115
+ focal_length = 1e9 # large enough to be effectively infinite
1116
+ else : # perspective projection
1117
+ focal_length = self ._focal_length
1118
+ eye = c + self ._view_w * self ._dist * r / self ._box_aspect * focal_length
1119
+ return eye
1120
+
1121
+ def _calc_coord (self , xv , yv , renderer = None ):
1122
+ """
1123
+ Given the 2D view coordinates, find the point on the nearest axis pane
1124
+ that lies directly below those coordinates. Returns a 3D point in data
1125
+ coordinates.
1126
+ """
1127
+ if self ._focal_length == np .inf : # orthographic projection
1128
+ zv = 1
1129
+ else : # perspective projection
1130
+ zv = - 1 / self ._focal_length
1131
+
1132
+ # Convert point on view plane to data coordinates
1133
+ p1 = np .array (proj3d .inv_transform (xv , yv , zv , self .invM )).ravel ()
1134
+
1135
+ # Get the vector from the camera to the point on the view plane
1136
+ vec = self ._get_camera_loc () - p1
1137
+
1138
+ # Get the pane locations for each of the axes
1139
+ pane_locs = []
1140
+ for axis in self ._axis_map .values ():
1141
+ xys , loc = axis .active_pane (renderer )
1142
+ pane_locs .append (loc )
1143
+
1144
+ # Find the distance to the nearest pane by projecting the view vector
1145
+ scales = np .zeros (3 )
1146
+ for i in range (3 ):
1147
+ if vec [i ] == 0 :
1148
+ scales [i ] = np .inf
1149
+ else :
1150
+ scales [i ] = (p1 [i ] - pane_locs [i ]) / vec [i ]
1151
+ scale = scales [np .argmin (abs (scales ))]
1152
+
1153
+ # Calculate the point on the closest pane
1154
+ p2 = p1 - scale * vec
1155
+ return p2
1102
1156
1103
1157
def _on_move (self , event ):
1104
1158
"""
@@ -1143,6 +1197,7 @@ def _on_move(self, event):
1143
1197
self .view_init (elev = elev , azim = azim , roll = roll , share = True )
1144
1198
self .stale = True
1145
1199
1200
+ # Pan
1146
1201
elif self .button_pressed in self ._pan_btn :
1147
1202
# Start the pan event with pixel coordinates
1148
1203
px , py = self .transData .transform ([self ._sx , self ._sy ])
@@ -1321,21 +1376,27 @@ def _scale_axis_limits(self, scale_x, scale_y, scale_z):
1321
1376
scale_z : float
1322
1377
Scale factor for the z data axis.
1323
1378
"""
1324
- # Get the axis limits and centers
1379
+ # Get the axis centers and ranges
1380
+ cx , cy , cz , dx , dy , dz = self ._get_w_centers_ranges ()
1381
+
1382
+ # Set the scaled axis limits
1383
+ self .set_xlim3d (cx - dx * scale_x / 2 , cx + dx * scale_x / 2 )
1384
+ self .set_ylim3d (cy - dy * scale_y / 2 , cy + dy * scale_y / 2 )
1385
+ self .set_zlim3d (cz - dz * scale_z / 2 , cz + dz * scale_z / 2 )
1386
+
1387
+ def _get_w_centers_ranges (self ):
1388
+ """Get 3D world centers and axis ranges."""
1389
+ # Calculate center of axis limits
1325
1390
minx , maxx , miny , maxy , minz , maxz = self .get_w_lims ()
1326
1391
cx = (maxx + minx )/ 2
1327
1392
cy = (maxy + miny )/ 2
1328
1393
cz = (maxz + minz )/ 2
1329
1394
1330
- # Scale the data range
1331
- dx = (maxx - minx )* scale_x
1332
- dy = (maxy - miny )* scale_y
1333
- dz = (maxz - minz )* scale_z
1334
-
1335
- # Set the scaled axis limits
1336
- self .set_xlim3d (cx - dx / 2 , cx + dx / 2 )
1337
- self .set_ylim3d (cy - dy / 2 , cy + dy / 2 )
1338
- self .set_zlim3d (cz - dz / 2 , cz + dz / 2 )
1395
+ # Calculate range of axis limits
1396
+ dx = (maxx - minx )
1397
+ dy = (maxy - miny )
1398
+ dz = (maxz - minz )
1399
+ return cx , cy , cz , dx , dy , dz
1339
1400
1340
1401
def set_zlabel (self , zlabel , fontdict = None , labelpad = None , ** kwargs ):
1341
1402
"""
0 commit comments