def add_texture_coords( mesh: pv.PolyData, meridian: Optional[float] = None, antimeridian: Optional[bool] = False, ) -> pv.PolyData: """ TODO Parameters ---------- mesh meridian antimeridian inplace Returns ------- Notes ----- .. versionadded:: 0.1.0 """ if meridian is None: meridian = DEFAULT_MERIDIAN if antimeridian: meridian += 180 meridian = wrap(meridian)[0] if GV_REMESH_POINT_IDS not in mesh.point_data: mesh = cut_along_meridian(mesh, meridian=meridian) else: mesh = mesh.copy(deep=True) # convert from cartesian xyz to spherical lat/lons ll = to_xy0(mesh, closed_interval=True) lons, lats = ll[:, 0], ll[:, 1] # convert to normalised UV space u = (lons + 180) / 360 v = (lats + 90) / 180 t = np.vstack([u, v]).T mesh.active_t_coords = t logger.debug( "u.min()=%s, u.max()=%s, v.min()=%s, v.max()=%s", u.min(), u.max(), v.min(), v.max(), ) return mesh
def plot_mesh_pyvista( plotter: pv.Plotter, polydata: pv.PolyData, # vertices: np.ndarray, # triangles: np.ndarray, rotations: List[Tuple[int, int, int]] = [(0, 0, 0)], vertexcolors: List[int] = [], vertexscalar: str = '', cmap: str = 'YlGnBu', title: str = '', scalar_bar_idx: int = 0, **mesh_kwargs, ): shape = plotter.shape if len(shape) == 1: assert shape[0] > 0 assert shape[0] == len(rotations) subp_idx = [(x, ) for x in range(shape[0])] else: assert shape[0] > 0 and shape[1] > 0 assert shape[0] * shape[1] == len(rotations) subp_idx = product(range(shape[0]), range(shape[1])) if vertexscalar and vertexcolors is not None: polydata[vertexscalar] = vertexcolors cmap = plt.cm.get_cmap(cmap) mesh_kwargs = { 'cmap': cmap, 'flip_scalars': True, 'show_scalar_bar': False, **mesh_kwargs, } if vertexscalar and vertexcolors is not None: mesh_kwargs['scalars'] = vertexscalar for i, (subp, rots) in enumerate(zip(subp_idx, rotations)): x, y, z = rots plotter.subplot(*subp) poly_copy = polydata.copy() poly_copy.rotate_x(x) poly_copy.rotate_y(y) poly_copy.rotate_z(z) plotter.add_mesh( poly_copy, **mesh_kwargs, ) if i == 0: plotter.add_title(title, font_size=5) if i == scalar_bar_idx: plotter.add_scalar_bar(label_font_size=10, position_x=0.85)
def cut_along_meridian( mesh: pv.PolyData, meridian: Optional[float] = None, antimeridian: Optional[bool] = False, ) -> pv.PolyData: """ TODO Parameters ---------- mesh meridian antimeridian Returns ------- Notes ----- .. versionadded:: 0.1.0 """ if not isinstance(mesh, pv.PolyData): emsg = f"Require a 'pyvista.PolyData' mesh, got '{mesh.__class__.__name__}'." raise TypeError(emsg) if meridian is None: meridian = DEFAULT_MERIDIAN if antimeridian: meridian += 180 meridian = wrap(meridian)[0] logger.debug( "meridian=%s, antimeridian=%s", meridian, antimeridian, ) slicer = MeridianSlice(mesh, meridian) mesh_whole = slicer.extract(split_cells=False) mesh_split = slicer.extract(split_cells=True) info = mesh.active_scalars_info result: pv.PolyData = mesh.copy(deep=True) meshes = [] remeshed_ids = np.array([]) if mesh_whole.n_cells: ll = to_xy0(mesh_whole) meridian_mask = np.isclose(ll[:, 0], meridian) join_points = np.empty(mesh_whole.n_points, dtype=int) join_points.fill(REMESH_JOIN) mesh_whole[GV_REMESH_POINT_IDS] = join_points mesh_whole[GV_REMESH_POINT_IDS][meridian_mask] = REMESH_SEAM meshes.append(mesh_whole) remeshed_ids = mesh_whole[GV_CELL_IDS] result[GV_REMESH_POINT_IDS] = result[GV_POINT_IDS].copy() if mesh_split.n_cells: remeshed, remeshed_west, remeshed_east = remesh(mesh_split, meridian) meshes.extend([remeshed_west, remeshed_east]) remeshed_ids = np.unique(np.hstack([remeshed_ids, remeshed[GV_CELL_IDS]])) if GV_REMESH_POINT_IDS not in result.point_data: result.point_data[GV_REMESH_POINT_IDS] = result[GV_POINT_IDS].copy() if meshes: result.remove_cells(remeshed_ids, inplace=True) result.set_active_scalars(info.name, preference=info.association.name.lower()) result = combine(result, *meshes) return result
def remesh( mesh: pv.PolyData, meridian: float, boundary: Optional[bool] = False, check: Optional[bool] = False, warnings: Optional[bool] = False, ) -> Remesh: """ TODO Notes ----- .. versionadded :: 0.1.0 """ if not warnings: # https://public.kitware.com/pipermail/vtkusers/2004-February/022390.html vtkObject.GlobalWarningDisplayOff() if mesh.n_cells == 0: emsg = "Cannot remesh an empty mesh" raise ValueError(emsg) meridian = wrap(meridian)[0] radius = calculate_radius(mesh) logger.debug( "meridian=%s, radius=%s", meridian, radius, ) poly0: pv.PolyData = mesh.copy(deep=True) if GV_CELL_IDS not in poly0.cell_data: poly0.cell_data[GV_CELL_IDS] = np.arange(poly0.n_cells) if GV_POINT_IDS not in poly0.point_data: poly0.point_data[GV_POINT_IDS] = np.arange(poly0.n_points) if not triangulated(poly0): start = datetime.now() poly0.triangulate(inplace=True) end = datetime.now() logger.debug( "mesh: triangulated [%s secs]", (end - start).total_seconds(), ) poly1 = pv.Plane( center=(radius / 2, 0, 0), i_resolution=1, j_resolution=1, i_size=radius, j_size=radius * 2, direction=(0, 1, 0), ) poly1.rotate_z(meridian, inplace=True) poly1.triangulate(inplace=True) # https://vtk.org/doc/nightly/html/classvtkIntersectionPolyDataFilter.html alg = _vtk.vtkIntersectionPolyDataFilter() alg.SetInputDataObject(0, poly0) alg.SetInputDataObject(1, poly1) # BoundaryPoints (points) mask array alg.SetComputeIntersectionPointArray(True) # BadTriangle and FreeEdge (cells) mask arrays alg.SetCheckMesh(check) alg.SetSplitFirstOutput(True) alg.SetSplitSecondOutput(False) start = datetime.now() alg.Update() end = datetime.now() logger.debug( "remeshed: lines=%s, points=%s [%s secs]", alg.GetNumberOfIntersectionLines(), alg.GetNumberOfIntersectionPoints(), (end - start).total_seconds(), ) remeshed: pv.PolyData = _get_output(alg, oport=1) if not warnings: vtkObject.GlobalWarningDisplayOn() if remeshed.n_cells == 0: # no remeshing has been performed as the meridian does not intersect the mesh remeshed_west, remeshed_east = pv.PolyData(), pv.PolyData() logger.debug( "no remesh performed using meridian=%s", meridian, ) else: # split the triangulated remesh into its two halves, west and east of the meridian centers = remeshed.cell_centers() lons = to_xy0(centers)[:, 0] delta = lons - meridian lower_mask = (delta < 0) & (delta > -180) upper_mask = delta > 180 west_mask = lower_mask | upper_mask east_mask = ~west_mask logger.debug( "split: lower=%s, upper=%s, west=%s, east=%s, total=%s", lower_mask.sum(), upper_mask.sum(), west_mask.sum(), east_mask.sum(), remeshed.n_cells, ) # the vtkIntersectionPolyDataFilter is configured to *always* generate the boundary mask point array # as we require it internally, regardless of whether the caller wants it or not afterwards boundary_mask = np.asarray(remeshed.point_data[VTK_BOUNDARY_MASK], dtype=bool) if not boundary: del remeshed.point_data[VTK_BOUNDARY_MASK] remeshed.point_data[GV_REMESH_POINT_IDS] = remeshed[GV_POINT_IDS].copy( ) remeshed[GV_REMESH_POINT_IDS][boundary_mask] = REMESH_SEAM remeshed_west = cast_UnstructuredGrid_to_PolyData( remeshed.extract_cells(west_mask)) join_mask = np.where( remeshed_west[GV_REMESH_POINT_IDS] != REMESH_SEAM)[0] remeshed_west[GV_REMESH_POINT_IDS][join_mask] = REMESH_JOIN remeshed[GV_REMESH_POINT_IDS][boundary_mask] = REMESH_SEAM_EAST remeshed_east = cast_UnstructuredGrid_to_PolyData( remeshed.extract_cells(east_mask)) join_mask = np.where( remeshed_east[GV_REMESH_POINT_IDS] != REMESH_SEAM_EAST)[0] remeshed_east[GV_REMESH_POINT_IDS][join_mask] = REMESH_JOIN del remeshed.point_data[GV_REMESH_POINT_IDS] sanitize_data(remeshed, remeshed_west, remeshed_east) return remeshed, remeshed_west, remeshed_east