@@ -75,7 +75,7 @@ def get_dir_vector(zdir):
7575
7676def _viewlim_mask (xs , ys , zs , axes ):
7777 """
78- Return original points with points outside the axes view limits masked .
78+ Return the mask of the points outside the axes view limits.
7979
8080 Parameters
8181 ----------
@@ -86,19 +86,16 @@ def _viewlim_mask(xs, ys, zs, axes):
8686
8787 Returns
8888 -------
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 .
9191 """
9292 mask = np .logical_or .reduce ((xs < axes .xy_viewLim .xmin ,
9393 xs > axes .xy_viewLim .xmax ,
9494 ys < axes .xy_viewLim .ymin ,
9595 ys > axes .xy_viewLim .ymax ,
9696 zs < axes .zz_viewLim .xmin ,
9797 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
10299
103100
104101class Text3D (mtext .Text ):
@@ -182,14 +179,13 @@ def set_3d_properties(self, z=0, zdir='z', axlim_clip=False):
182179 @artist .allow_rasterization
183180 def draw (self , renderer ):
184181 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 )
187185 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 )
190187
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 )
193189 dx = proj [0 ][1 ] - proj [0 ][0 ]
194190 dy = proj [1 ][1 ] - proj [1 ][0 ]
195191 angle = math .degrees (math .atan2 (dy , dx ))
@@ -313,7 +309,12 @@ def get_data_3d(self):
313309 @artist .allow_rasterization
314310 def draw (self , renderer ):
315311 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 )
317318 else :
318319 xs3d , ys3d , zs3d = self ._verts3d
319320 xs , ys , zs , tis = proj3d ._proj_transform_clip (xs3d , ys3d , zs3d ,
@@ -404,7 +405,8 @@ def do_3d_projection(self):
404405 """Project the points according to renderer matrix."""
405406 vs_list = [vs for vs , _ in self ._3dverts_codes ]
406407 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 ))
408410 for vs in vs_list ]
409411 xyzs_list = [proj3d .proj_transform (* vs .T , self .axes .M ) for vs in vs_list ]
410412 self ._paths = [mpath .Path (np .ma .column_stack ([xs , ys ]), cs )
@@ -450,22 +452,32 @@ def do_3d_projection(self):
450452 """
451453 Project the points according to renderer matrix.
452454 """
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+
454461 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 ]
463474 LineCollection .set_segments (self , segments_2d )
464475
465476 # 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
469481 return minz
470482
471483
@@ -531,7 +543,9 @@ def get_path(self):
531543 def do_3d_projection (self ):
532544 s = self ._segment3d
533545 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 )
535549 else :
536550 xs , ys , zs = zip (* s )
537551 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):
587601 def do_3d_projection (self ):
588602 s = self ._segment3d
589603 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 )
591607 else :
592608 xs , ys , zs = zip (* s )
593609 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):
701717
702718 def do_3d_projection (self ):
703719 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 )
705722 else :
706723 xs , ys , zs = self ._offsets3d
707724 vxs , vys , vzs , vis = proj3d ._proj_transform_clip (xs , ys , zs ,
708725 self .axes .M ,
709726 self .axes ._focal_length )
710727 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 ]))
712732
713733 if vzs .size > 0 :
714734 return min (vzs )
@@ -851,11 +871,18 @@ def set_depthshade(self, depthshade):
851871 self .stale = True
852872
853873 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
854878 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 )
856883 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 ,
859886 self .axes .M ,
860887 self .axes ._focal_length )
861888 # Sort the points based on z coordinates
@@ -1062,16 +1089,37 @@ def get_vector(self, segments3d):
10621089 return self ._get_vector (segments3d )
10631090
10641091 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.
10721094
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 ]
10751123
10761124 def set_verts (self , verts , closed = True ):
10771125 """
@@ -1133,64 +1181,85 @@ def do_3d_projection(self):
11331181 self ._facecolor3d = self ._facecolors
11341182 if self ._edge_is_mapped :
11351183 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+
11361194 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 )
11471204
11481205 # This extra fuss is to re-order face / edge colors
11491206 cface = self ._facecolor3d
11501207 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 :
11541211 if len (cedge ) == 0 :
11551212 cedge = cface
11561213 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 )
11781218 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 ]
11801223
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 :
11821251 self ._edgecolors2d = self ._edgecolor3d
11831252
11841253 # Return zorder value
11851254 if self ._sort_zpos is not None :
11861255 zvec = np .array ([[0 ], [0 ], [self ._sort_zpos ], [1 ]])
11871256 ztrans = proj3d ._proj_transform_vec (zvec , self .axes .M )
11881257 return ztrans [2 ][0 ]
1189- elif tzs .size > 0 :
1258+ elif pzs .size > 0 :
11901259 # FIXME: Some results still don't look quite right.
11911260 # In particular, examine contourf3d_demo2.py
11921261 # with az = -54 and elev = -45.
1193- return np .min (tzs )
1262+ return np .min (pzs )
11941263 else :
11951264 return np .nan
11961265
0 commit comments