@@ -75,7 +75,7 @@ def get_dir_vector(zdir):
75
75
76
76
def _viewlim_mask (xs , ys , zs , axes ):
77
77
"""
78
- Return original points with points outside the axes view limits masked .
78
+ Return the mask of the points outside the axes view limits.
79
79
80
80
Parameters
81
81
----------
@@ -86,19 +86,16 @@ def _viewlim_mask(xs, ys, zs, axes):
86
86
87
87
Returns
88
88
-------
89
- xs_masked, ys_masked, zs_masked : np.ma .array
90
- The masked points.
89
+ mask : np.array
90
+ The mask of the points as a bool array .
91
91
"""
92
92
mask = np .logical_or .reduce ((xs < axes .xy_viewLim .xmin ,
93
93
xs > axes .xy_viewLim .xmax ,
94
94
ys < axes .xy_viewLim .ymin ,
95
95
ys > axes .xy_viewLim .ymax ,
96
96
zs < axes .zz_viewLim .xmin ,
97
97
zs > axes .zz_viewLim .xmax ))
98
- xs_masked = np .ma .array (xs , mask = mask )
99
- ys_masked = np .ma .array (ys , mask = mask )
100
- zs_masked = np .ma .array (zs , mask = mask )
101
- return xs_masked , ys_masked , zs_masked
98
+ return mask
102
99
103
100
104
101
class Text3D (mtext .Text ):
@@ -182,14 +179,13 @@ def set_3d_properties(self, z=0, zdir='z', axlim_clip=False):
182
179
@artist .allow_rasterization
183
180
def draw (self , renderer ):
184
181
if self ._axlim_clip :
185
- xs , ys , zs = _viewlim_mask (self ._x , self ._y , self ._z , self .axes )
186
- position3d = np .ma .row_stack ((xs , ys , zs )).ravel ().filled (np .nan )
182
+ mask = _viewlim_mask (self ._x , self ._y , self ._z , self .axes )
183
+ pos3d = np .ma .array ([self ._x , self ._y , self ._z ],
184
+ mask = mask , dtype = float ).filled (np .nan )
187
185
else :
188
- xs , ys , zs = self ._x , self ._y , self ._z
189
- position3d = np .asanyarray ([xs , ys , zs ])
186
+ pos3d = np .array ([self ._x , self ._y , self ._z ], dtype = float )
190
187
191
- proj = proj3d ._proj_trans_points (
192
- [position3d , position3d + self ._dir_vec ], self .axes .M )
188
+ proj = proj3d ._proj_trans_points ([pos3d , pos3d + self ._dir_vec ], self .axes .M )
193
189
dx = proj [0 ][1 ] - proj [0 ][0 ]
194
190
dy = proj [1 ][1 ] - proj [1 ][0 ]
195
191
angle = math .degrees (math .atan2 (dy , dx ))
@@ -313,7 +309,12 @@ def get_data_3d(self):
313
309
@artist .allow_rasterization
314
310
def draw (self , renderer ):
315
311
if self ._axlim_clip :
316
- xs3d , ys3d , zs3d = _viewlim_mask (* self ._verts3d , self .axes )
312
+ mask = np .broadcast_to (
313
+ _viewlim_mask (* self ._verts3d , self .axes ),
314
+ (len (self ._verts3d ), * self ._verts3d [0 ].shape )
315
+ )
316
+ xs3d , ys3d , zs3d = np .ma .array (self ._verts3d ,
317
+ dtype = float , mask = mask ).filled (np .nan )
317
318
else :
318
319
xs3d , ys3d , zs3d = self ._verts3d
319
320
xs , ys , zs , tis = proj3d ._proj_transform_clip (xs3d , ys3d , zs3d ,
@@ -404,7 +405,8 @@ def do_3d_projection(self):
404
405
"""Project the points according to renderer matrix."""
405
406
vs_list = [vs for vs , _ in self ._3dverts_codes ]
406
407
if self ._axlim_clip :
407
- vs_list = [np .ma .row_stack (_viewlim_mask (* vs .T , self .axes )).T
408
+ vs_list = [np .ma .array (vs , mask = np .broadcast_to (
409
+ _viewlim_mask (* vs .T , self .axes ), vs .shape ))
408
410
for vs in vs_list ]
409
411
xyzs_list = [proj3d .proj_transform (* vs .T , self .axes .M ) for vs in vs_list ]
410
412
self ._paths = [mpath .Path (np .ma .column_stack ([xs , ys ]), cs )
@@ -450,22 +452,32 @@ def do_3d_projection(self):
450
452
"""
451
453
Project the points according to renderer matrix.
452
454
"""
453
- segments = self ._segments3d
455
+ segments = np .asanyarray (self ._segments3d )
456
+
457
+ mask = False
458
+ if np .ma .isMA (segments ):
459
+ mask = segments .mask
460
+
454
461
if self ._axlim_clip :
455
- all_points = np .ma .vstack (segments )
456
- masked_points = np .ma .column_stack ([* _viewlim_mask (* all_points .T ,
457
- self .axes )])
458
- segment_lengths = [np .shape (segment )[0 ] for segment in segments ]
459
- segments = np .split (masked_points , np .cumsum (segment_lengths [:- 1 ]))
460
- xyslist = [proj3d ._proj_trans_points (points , self .axes .M )
461
- for points in segments ]
462
- segments_2d = [np .ma .column_stack ([xs , ys ]) for xs , ys , zs in xyslist ]
462
+ viewlim_mask = _viewlim_mask (segments [..., 0 ],
463
+ segments [..., 1 ],
464
+ segments [..., 2 ],
465
+ self .axes )
466
+ if np .any (viewlim_mask ):
467
+ # broadcast mask to 3D
468
+ viewlim_mask = np .broadcast_to (viewlim_mask [..., np .newaxis ],
469
+ (* viewlim_mask .shape , 3 ))
470
+ mask = mask | viewlim_mask
471
+ xyzs = np .ma .array (proj3d ._proj_transform_vectors (segments , self .axes .M ),
472
+ mask = mask )
473
+ segments_2d = xyzs [..., 0 :2 ]
463
474
LineCollection .set_segments (self , segments_2d )
464
475
465
476
# FIXME
466
- minz = 1e9
467
- for xs , ys , zs in xyslist :
468
- minz = min (minz , min (zs ))
477
+ if len (xyzs ) > 0 :
478
+ minz = min (xyzs [..., 2 ].min (), 1e9 )
479
+ else :
480
+ minz = np .nan
469
481
return minz
470
482
471
483
@@ -531,7 +543,9 @@ def get_path(self):
531
543
def do_3d_projection (self ):
532
544
s = self ._segment3d
533
545
if self ._axlim_clip :
534
- xs , ys , zs = _viewlim_mask (* zip (* s ), self .axes )
546
+ mask = _viewlim_mask (* zip (* s ), self .axes )
547
+ xs , ys , zs = np .ma .array (zip (* s ),
548
+ dtype = float , mask = mask ).filled (np .nan )
535
549
else :
536
550
xs , ys , zs = zip (* s )
537
551
vxs , vys , vzs , vis = proj3d ._proj_transform_clip (xs , ys , zs ,
@@ -587,7 +601,9 @@ def set_3d_properties(self, path, zs=0, zdir='z', axlim_clip=False):
587
601
def do_3d_projection (self ):
588
602
s = self ._segment3d
589
603
if self ._axlim_clip :
590
- xs , ys , zs = _viewlim_mask (* zip (* s ), self .axes )
604
+ mask = _viewlim_mask (* zip (* s ), self .axes )
605
+ xs , ys , zs = np .ma .array (zip (* s ),
606
+ dtype = float , mask = mask ).filled (np .nan )
591
607
else :
592
608
xs , ys , zs = zip (* s )
593
609
vxs , vys , vzs , vis = proj3d ._proj_transform_clip (xs , ys , zs ,
@@ -701,14 +717,18 @@ def set_3d_properties(self, zs, zdir, axlim_clip=False):
701
717
702
718
def do_3d_projection (self ):
703
719
if self ._axlim_clip :
704
- xs , ys , zs = _viewlim_mask (* self ._offsets3d , self .axes )
720
+ mask = _viewlim_mask (* self ._offsets3d , self .axes )
721
+ xs , ys , zs = np .ma .array (self ._offsets3d , mask = mask )
705
722
else :
706
723
xs , ys , zs = self ._offsets3d
707
724
vxs , vys , vzs , vis = proj3d ._proj_transform_clip (xs , ys , zs ,
708
725
self .axes .M ,
709
726
self .axes ._focal_length )
710
727
self ._vzs = vzs
711
- super ().set_offsets (np .ma .column_stack ([vxs , vys ]))
728
+ if np .ma .isMA (vxs ):
729
+ super ().set_offsets (np .ma .column_stack ([vxs , vys ]))
730
+ else :
731
+ super ().set_offsets (np .column_stack ([vxs , vys ]))
712
732
713
733
if vzs .size > 0 :
714
734
return min (vzs )
@@ -851,11 +871,18 @@ def set_depthshade(self, depthshade):
851
871
self .stale = True
852
872
853
873
def do_3d_projection (self ):
874
+ mask = False
875
+ for xyz in self ._offsets3d :
876
+ if np .ma .isMA (xyz ):
877
+ mask = mask | xyz .mask
854
878
if self ._axlim_clip :
855
- xs , ys , zs = _viewlim_mask (* self ._offsets3d , self .axes )
879
+ mask = mask | _viewlim_mask (* self ._offsets3d , self .axes )
880
+ mask = np .broadcast_to (mask ,
881
+ (len (self ._offsets3d ), * self ._offsets3d [0 ].shape ))
882
+ xyzs = np .ma .array (self ._offsets3d , mask = mask )
856
883
else :
857
- xs , ys , zs = self ._offsets3d
858
- vxs , vys , vzs , vis = proj3d ._proj_transform_clip (xs , ys , zs ,
884
+ xyzs = self ._offsets3d
885
+ vxs , vys , vzs , vis = proj3d ._proj_transform_clip (* xyzs ,
859
886
self .axes .M ,
860
887
self .axes ._focal_length )
861
888
# Sort the points based on z coordinates
@@ -1062,16 +1089,37 @@ def get_vector(self, segments3d):
1062
1089
return self ._get_vector (segments3d )
1063
1090
1064
1091
def _get_vector (self , segments3d ):
1065
- """Optimize points for projection."""
1066
- if len (segments3d ):
1067
- xs , ys , zs = np .vstack (segments3d ).T
1068
- else : # vstack can't stack zero arrays.
1069
- xs , ys , zs = [], [], []
1070
- ones = np .ones (len (xs ))
1071
- self ._vec = np .array ([xs , ys , zs , ones ])
1092
+ """
1093
+ Optimize points for projection.
1072
1094
1073
- indices = [0 , * np .cumsum ([len (segment ) for segment in segments3d ])]
1074
- self ._segslices = [* map (slice , indices [:- 1 ], indices [1 :])]
1095
+ Parameters
1096
+ ----------
1097
+ segments3d : NumPy array or list of NumPy arrays
1098
+ List of vertices of the boundary of every segment. If all paths are
1099
+ of equal length and this argument is a NumPy array, then it should
1100
+ be of shape (num_faces, num_vertices, 3).
1101
+ """
1102
+ if isinstance (segments3d , np .ndarray ):
1103
+ if segments3d .ndim != 3 or segments3d .shape [- 1 ] != 3 :
1104
+ raise ValueError ("segments3d must be a MxNx3 array, but got "
1105
+ f"shape { segments3d .shape } " )
1106
+ if isinstance (segments3d , np .ma .MaskedArray ):
1107
+ self ._faces = segments3d .data
1108
+ self ._invalid_vertices = segments3d .mask .any (axis = - 1 )
1109
+ else :
1110
+ self ._faces = segments3d
1111
+ self ._invalid_vertices = False
1112
+ else :
1113
+ # Turn the potentially ragged list into a numpy array for later speedups
1114
+ # If it is ragged, set the unused vertices per face as invalid
1115
+ num_faces = len (segments3d )
1116
+ num_verts = np .fromiter (map (len , segments3d ), dtype = np .intp )
1117
+ max_verts = num_verts .max (initial = 0 )
1118
+ segments = np .empty ((num_faces , max_verts , 3 ))
1119
+ for i , face in enumerate (segments3d ):
1120
+ segments [i , :len (face )] = face
1121
+ self ._faces = segments
1122
+ self ._invalid_vertices = np .arange (max_verts ) >= num_verts [:, None ]
1075
1123
1076
1124
def set_verts (self , verts , closed = True ):
1077
1125
"""
@@ -1133,64 +1181,85 @@ def do_3d_projection(self):
1133
1181
self ._facecolor3d = self ._facecolors
1134
1182
if self ._edge_is_mapped :
1135
1183
self ._edgecolor3d = self ._edgecolors
1184
+
1185
+ needs_masking = np .any (self ._invalid_vertices )
1186
+ num_faces = len (self ._faces )
1187
+ mask = self ._invalid_vertices
1188
+
1189
+ # Some faces might contain masked vertices, so we want to ignore any
1190
+ # errors that those might cause
1191
+ with np .errstate (invalid = 'ignore' , divide = 'ignore' ):
1192
+ pfaces = proj3d ._proj_transform_vectors (self ._faces , self .axes .M )
1193
+
1136
1194
if self ._axlim_clip :
1137
- xs , ys , zs = _viewlim_mask (* self ._vec [0 :3 ], self .axes )
1138
- if self ._vec .shape [0 ] == 4 : # Will be 3 (xyz) or 4 (xyzw)
1139
- w_masked = np .ma .masked_where (zs .mask , self ._vec [3 ])
1140
- vec = np .ma .array ([xs , ys , zs , w_masked ])
1141
- else :
1142
- vec = np .ma .array ([xs , ys , zs ])
1143
- else :
1144
- vec = self ._vec
1145
- txs , tys , tzs = proj3d ._proj_transform_vec (vec , self .axes .M )
1146
- xyzlist = [(txs [sl ], tys [sl ], tzs [sl ]) for sl in self ._segslices ]
1195
+ viewlim_mask = _viewlim_mask (self ._faces [..., 0 ], self ._faces [..., 1 ],
1196
+ self ._faces [..., 2 ], self .axes )
1197
+ if np .any (viewlim_mask ):
1198
+ needs_masking = True
1199
+ mask = mask | viewlim_mask
1200
+
1201
+ pzs = pfaces [..., 2 ]
1202
+ if needs_masking :
1203
+ pzs = np .ma .MaskedArray (pzs , mask = mask )
1147
1204
1148
1205
# This extra fuss is to re-order face / edge colors
1149
1206
cface = self ._facecolor3d
1150
1207
cedge = self ._edgecolor3d
1151
- if len (cface ) != len ( xyzlist ) :
1152
- cface = cface .repeat (len ( xyzlist ) , axis = 0 )
1153
- if len (cedge ) != len ( xyzlist ) :
1208
+ if len (cface ) != num_faces :
1209
+ cface = cface .repeat (num_faces , axis = 0 )
1210
+ if len (cedge ) != num_faces :
1154
1211
if len (cedge ) == 0 :
1155
1212
cedge = cface
1156
1213
else :
1157
- cedge = cedge .repeat (len (xyzlist ), axis = 0 )
1158
-
1159
- if xyzlist :
1160
- # sort by depth (furthest drawn first)
1161
- z_segments_2d = sorted (
1162
- ((self ._zsortfunc (zs .data ), np .ma .column_stack ([xs , ys ]), fc , ec , idx )
1163
- for idx , ((xs , ys , zs ), fc , ec )
1164
- in enumerate (zip (xyzlist , cface , cedge ))),
1165
- key = lambda x : x [0 ], reverse = True )
1166
-
1167
- _ , segments_2d , self ._facecolors2d , self ._edgecolors2d , idxs = \
1168
- zip (* z_segments_2d )
1169
- else :
1170
- segments_2d = []
1171
- self ._facecolors2d = np .empty ((0 , 4 ))
1172
- self ._edgecolors2d = np .empty ((0 , 4 ))
1173
- idxs = []
1174
-
1175
- if self ._codes3d is not None :
1176
- codes = [self ._codes3d [idx ] for idx in idxs ]
1177
- PolyCollection .set_verts_and_codes (self , segments_2d , codes )
1214
+ cedge = cedge .repeat (num_faces , axis = 0 )
1215
+
1216
+ if len (pzs ) > 0 :
1217
+ face_z = self ._zsortfunc (pzs , axis = - 1 )
1178
1218
else :
1179
- PolyCollection .set_verts (self , segments_2d , self ._closed )
1219
+ face_z = pzs
1220
+ if needs_masking :
1221
+ face_z = face_z .data
1222
+ face_order = np .argsort (face_z , axis = - 1 )[::- 1 ]
1180
1223
1181
- if len (self ._edgecolor3d ) != len (cface ):
1224
+ if len (pfaces ) > 0 :
1225
+ faces_2d = pfaces [face_order , :, :2 ]
1226
+ else :
1227
+ faces_2d = pfaces
1228
+ if self ._codes3d is not None and len (self ._codes3d ) > 0 :
1229
+ if needs_masking :
1230
+ segment_mask = ~ mask [face_order , :]
1231
+ faces_2d = [face [mask , :] for face , mask
1232
+ in zip (faces_2d , segment_mask )]
1233
+ codes = [self ._codes3d [idx ] for idx in face_order ]
1234
+ PolyCollection .set_verts_and_codes (self , faces_2d , codes )
1235
+ else :
1236
+ if needs_masking and len (faces_2d ) > 0 :
1237
+ invalid_vertices_2d = np .broadcast_to (
1238
+ mask [face_order , :, None ],
1239
+ faces_2d .shape )
1240
+ faces_2d = np .ma .MaskedArray (
1241
+ faces_2d , mask = invalid_vertices_2d )
1242
+ PolyCollection .set_verts (self , faces_2d , self ._closed )
1243
+
1244
+ if len (cface ) > 0 :
1245
+ self ._facecolors2d = cface [face_order ]
1246
+ else :
1247
+ self ._facecolors2d = cface
1248
+ if len (self ._edgecolor3d ) == len (cface ) and len (cedge ) > 0 :
1249
+ self ._edgecolors2d = cedge [face_order ]
1250
+ else :
1182
1251
self ._edgecolors2d = self ._edgecolor3d
1183
1252
1184
1253
# Return zorder value
1185
1254
if self ._sort_zpos is not None :
1186
1255
zvec = np .array ([[0 ], [0 ], [self ._sort_zpos ], [1 ]])
1187
1256
ztrans = proj3d ._proj_transform_vec (zvec , self .axes .M )
1188
1257
return ztrans [2 ][0 ]
1189
- elif tzs .size > 0 :
1258
+ elif pzs .size > 0 :
1190
1259
# FIXME: Some results still don't look quite right.
1191
1260
# In particular, examine contourf3d_demo2.py
1192
1261
# with az = -54 and elev = -45.
1193
- return np .min (tzs )
1262
+ return np .min (pzs )
1194
1263
else :
1195
1264
return np .nan
1196
1265
0 commit comments