Pyvista reads VTK files to render 3D cloud images and slice and crop them (1)

Pyvista official documentation: https://qtdocs.pyvista.org/index.html
The following code was written by me after checking the official documentation API, and it contains the translation of the official API. The notes are very detailed, so the author won’t go into details.
Three-dimensional cloud drawing effect:
Surface:
surface
Surface + Mesh:
Surface + Mesh:
Gridlines:
grid lines
Surface Points + Right-click to select Show Data:
Surface point + right-click to select display data
Gaussian point + right-click to select display data:
Right-click the selected point to display its data value.

Slicing and cropping
Box cropping:
The hexahedron where the box is located can be dragged normally on each face, and the box can also be rotated and moved.


Flat cropping:
The arrow is the plane where the normal line is located to crop the model. You can drag the arrow and the tail of the arrow to change the angle of the plane, or you can drag the plane in the normal direction.



Plane slice:
The arrow is the plane where the normal line is located to slice the model. You can drag the arrow and the tail of the arrow to change the angle of the plane, or you can drag the plane in the normal direction.

Three-plane slices:
Three mutually perpendicular planes can be dragged in their normal directions for slicing.


Surface slice:
The surface where the line formed by the ball is located is sliced, and the ball can be dragged at will.




Data threshold clipping:
Drag the slider to change the threshold. The data on the right side of the default slider (>= slider value) is displayed and can be reversed.


import numpy as np
import pyvista as pv
from pyvista.plotting.opts import PickerType

vtu_filename = "../Data/cylinderdemo4_out.vtu"
# Read VTK file
mesh = pv.read(vtu_filename)
#Read node data
node_data_name = "Temperature" # Replace with your node data name
node_data = mesh.point_data[node_data_name]



plotter = pv.Plotter(shape="1|1") # TODO shape="3|1", separate the drawing windows, 3 on the left and one on the right
print(plotter)
plotter.subplot(0, 0) # TODO draw in the first box
cylinder = plotter.add_mesh(mesh, scalars=node_data, cmap="coolwarm",
                 # show_scalar_bar=False,
                 scalar_bar_args={<!-- -->'title': 'Temperature',
                                  "vertical":"False",
                                  "height":0.65,
                                  "position_x":0.85,
                                  "position_y":0.1},
                 # show_vertices=True, # TODO show vertices
                 point_size=3, #TODO point size
                 # style='points_gaussian',
                 # show_edges=True, # TODO Show grid
                 style='surface', # TODO Default display style: 'surface' surface, 'wireframe' lines, 'points' surface points match the point_size setting size,
                 ) # TODO Cylindrical

_ = plotter.add_axes( # TODO Lower left corner coordinate axis
    line_width=5,
    cone_radius=0.6,
    shaft_length=0.7,
    tip_length=0.3,
    ambient=0.5, ambient
    label_size=(0.4, 0.16),
)
plotter.subplot(1, 0)
# TODO Add a slicing box to clip the mesh. Save the clipped mesh to the plotter.box_clipped_meshes property.
# plotter.add_mesh_clip_box(mesh,
# interaction_event='end', # When does TODO trigger a change? 'start' just starts dragging, 'end' drags and releases, and 'always' keeps changing.
# show_scalar_bar=False,
# cmap="coolwarm",
# ) # TODO slice
actor =plotter.add_mesh_clip_box(mesh,
                          invert=False, # TODO Flag whether to flip/reverse the clip
                          rotation_enabled=True, # TODO If False, the box widget cannot rotate and is strictly orthogonal to the Cartesian axis
                          widget_color=None, # TODO The color of the widget. String, RGB sequence, or hexadecimal color string, as color='white'
                          outline_translation=True, # TODO If False, flat widgets cannot be translated and are placed strictly within the given bounds
                          merge_points=True, # TODO If True (default), coincident points of independently defined grid elements will be merged
                          crinkle=False, # TODO Crinkle the clip by extracting the entire cell along the clip
                          interaction_event='end', # When does TODO trigger a change? 'start' just starts dragging, 'end' releases dragging, and 'always' keeps changing.
                          # **kwargs # TODO All parameters of add_mesh() can be used
                          cmap="coolwarm",
                          )
# print(type(actor))
# print(actor)
# clip = plotter.box_clipped_meshes
# print(type(clip))

# TODO Use plane to clip the mesh. Keep the remaining 3D mesh. Save the clipped mesh to the plotter.plane_clipped_meshes property.
# plotter.add_mesh_clip_plane(mesh,
# normal='x', # The starting normal vector of the TODO plane
# invert=False, # TODO Flag whether to flip/reverse the clip
# widget_color=None, # TODO component color string, RGB list or hexadecimal color string
# value=0.0, # TODO Set the clipping value along the normal direction. The default value is 0.0
# assign_to_axis=None, # TODO Specifies that the plane's normal is parallel to the given axis. Options are (0, 'x'), (1, 'y'), or (2, 'z')
# tubing=False, # TODO When using the implicit planar wiget, this controls whether tubing is shown around planar boundaries
# origin_translation=True, # TODO If False, the planar widget cannot be translated through its origin, but is placed strictly at the given origin. Only valid when using implicit planes
# outline_translation=False, # TODO If False, the box widget cannot be translated and is placed strictly within the given bounds.
# implicit=True, # TODO When it is True, use vtkImplicitPlaneWidget, when it is False, use vtkPlaneWidget.
# normal_rotation=True, # TODO Set the opacity of the normal vector arrow to 0 so that it is effectively disabled. This prevents the user from rotating the normals. When assign_to_axis is set, this value is forced to False.
# crinkle=False, # TODO Crinkle the clip by extracting the entire cell along the clip
# interaction_event='end', # When does TODO trigger a change? 'start' just starts dragging, 'end' drags and releases, and 'always' keeps changing.
# origin=None, # TODO The starting coordinate of the plane center
# # **kwargs # TODO All parameters of add_mesh() can be used
# cmap="coolwarm",
# )


# TODO Use plane to clip the mesh. Keep the 2D mesh where the plane is located. The sliced mesh is saved to the plotter.plane_sliced_meshes property.
# plotter.add_mesh_slice(mesh,
# normal='x', # The starting normal vector of the TODO plane
# generate_triangles=False, # TODO If this is enabled (False by default), the output will be triangles, otherwise, the output will be intersecting polygons
# widget_color=None, # TODO The color of the flat component is a string, RGB sequence or hexadecimal color string. Default is'white'
# assign_to_axis=None, # TODO Specifies that the normal of the plane is parallel to the given axis: options are (0,' x '), (1, ' y ') or (2, ' z ')
# tubing=False, # TODO When using the implicit planar wiget, this controls whether tubing is shown around planar boundaries
# origin_translation=True, # TODO If False, the planar widget cannot be translated through its origin, but is placed strictly at the given origin. Only valid when using implicit planes
# outline_translation=False, # TODO If False, the box widget cannot be translated and is placed strictly on the given bounds.
# implicit=True, # TODO When it is True, use vtkImplicitPlanewidget, when it is False, use vtkPlaneWidget.
# normal_rotation=True, # TODO Set the opacity of the normal vector arrow to 0 so that it is effectively disabled. This prevents the user from rotating the normals. When assign_to_axis is set, this value is forced to False.
# interaction_event="end", # When is TODO triggered (event id) 45: End interaction 44: When dragging
# origin=None, # TODO The starting coordinates of the plane center.
# # **kwargs # TODO All parameters of add_mesh() can be used
# cmap="coolwarm",
# )


# TODO Cut the grid with three mutually perpendicular planes and retain the two-dimensional grid where the three planes are located.
# plotter.add_mesh_slice_orthogonal(mesh,
# generate_triangles=False, # TODO If this is enabled (False by default), the output will be triangles, otherwise, the output will be intersecting polygons
# widget_color=None, # Color of TODO widget. String, RGB sequence, or hexadecimal color string, as color='white'
# tubing=False, # TODO When using the implicit planar wiget, this controls whether tubing is shown around planar boundaries
# interaction_event='end', # When does TODO trigger a change? 'start' just starts dragging, 'end' drags and releases, and 'always' keeps changing.
# # **kwargs # TODO All parameters of add_mesh() can be used
# cmap="coolwarm",
# )

# TODO Use the spline widget (multi-point draggable line) to cut the mesh. Preserve the plane and surface mesh composed of the lines. The sliced mesh is saved to the plotter.spline_sliced_meshes property.
# plotter.add_mesh_slice_spline(mesh,
# generate_triangles=False, # TODO If this is enabled (False by default), the output will be triangles, otherwise, the output will be intersecting polygons
# n_handles=5, # TODO Control the number of interactive spheres for the spline parameter function
# resolution=25, # TODO Number of points to generate on the spline
# widget_color=None, # Color of TODO widget. String, RGB sequence, or hexadecimal color string, as color='white'
# show_ribbon=True, # TODO If True, the polygon plane used for slicing will also be shown
# ribbon_color='pink', # TODO The color of the ribbon. String, RGB sequence, or hexadecimal color string
# ribbon_opacity=0.5, # TODO The opacity of the ribbon. Default value is 1.0, must be between [0, 1]
# initial_points=None, # TODO Initialize the points of the widget position. Must have the same number of elements as n_handles. If the first and last points are the same, this will be a closed loop spline
# closed=False, # TODO Make the spline a closed loop
# interaction_event=44, # When is TODO triggered (event id) 45: End interaction 44: When dragging
# # **kwargs # TODO All parameters of add_mesh() can be used
# cmap="coolwarm",
# )

# TODO Apply threshold on grid using slider
# plotter.add_mesh_threshold(mesh,
# show_scalar_bar=False,
# cmap="coolwarm",
# ) # TODO
# plotter.add_mesh_threshold(mesh,
# scalars=None, # TODO The string name of the scalar on the grid to set the threshold and display
# invert=False, # TODO invert threshold results. That is, cells in the output will be excluded while cells in the output will be included when this option is turned off
# widget_color=None, # Color of TODO widget. String, RGB sequence, or hexadecimal color string. as color='white'
# preference='cell', # TODO This parameter sets how scalars are mapped to grids. Defaults to 'cell', causing the scalar to be associated with a grid cell. It can be 'point' or 'cell'
# title=None, # String label for TODO slider widget
# pointa=(0.4, 0.9), # TODO Display the relative coordinates of the point on the left side of the slider on the port
# pointb=(0.9, 0.9),# TODO displays the relative coordinates of the right point of the slider on the port
# continuous=False, # TODO If this option is enabled (default is False), use continuous intervals [minimum cell scalar, maximum cell scalar] to intersect threshold boundaries instead of intersecting sets of discrete scalar values for vertices
# all_scalars=False, # TODO If using scalars for point data, when the value is True, all points in the cell must meet the threshold. When False, any point with a scalar value for the cell that meets the threshold criteria will extract the cell. Not valid when using cell data
# method='upper', # TODO Set the threshold method for a single value, defining the threshold limit to be used. If value is a range, this parameter will be ignored and the data between the two values will be extracted. For a single value, 'lower' will extract lower than value. 'upper' will extract greater than value.
# # **kwargs # TODO All parameters of add_mesh() can be used
# cmap="coolwarm",
# )



# TODO Add a label to the specified point. Later, you can try to click a point with the mouse to add a value label to it.
# point_A = pv.pyvista_ndarray([mesh.points[0]])
# label_A = pv.pyvista_ndarray([node_data[0]])
# print(point_A)
# print(type(point_A))
# plotter.add_point_labels(points=point_A, labels=label_A)

# TODO Add the outline to the scene. It can be used during cutting for easy observation.
# plotter.add_silhouette(mesh,
# color=None, # TODO outline line color
# line_width=None, # TODO outline line width
# opacity=None, # TODO Line opacity between 0 and 1
# feature_angle=None, # TODO If set, show sharp edges that exceed this angle
# decimate=None, # TODO between decimation levels 0 and 1. Decimation will improve rendering performance. A good rule of thumb is to try 0.9 until you achieve the desired rendering performance
# )









# plotter.subplot(1, 0) # TODO draw in the second box
# plotter.add_mesh(mesh, scalars=node_data, cmap="coolwarm",
# show_scalar_bar=False,
# # scalar_bar_args={<!-- -->
# # 'title': '',
# # "vertical":"False",
# # "height":0.65,
# # "position_x":0.85,
# # "position_y":0.1},
# # show_vertices=True, # TODO Show vertices
# # point_size=5, # TODO point size
# # style='points_gaussian',
# show_edges=True, # TODO Show grid
#
# ) # TODO Cylindrical
#
# _ = plotter.add_axes( # TODO Lower left corner coordinate axis
# line_width=5,
# cone_radius=0.6,
# shaft_length=0.7,
# tip_length=0.3,
#ambient=0.5,
# label_size=(0.4, 0.16),
# )

# plotter.subplot(2, 0) # TODO draw in the third box
# plotter.add_mesh(mesh, scalars=node_data, cmap="coolwarm",
# show_scalar_bar=False,
# # scalar_bar_args={<!-- -->
# # 'title': 't',
# # "vertical":"False",
# # "height":0.65,
# # "position_x":0.85,
# # "position_y":0.1},
# show_vertices=True, # TODO show vertices
# # point_size=5, # TODO point size
# # style='points_gaussian',
# # show_edges=True, # TODO Show grid
#
# ) # TODO Cylindrical
#
# _ = plotter.add_axes( # TODO Lower left corner coordinate axis
# line_width=5,
# cone_radius=0.6,
# shaft_length=0.7,
# tip_length=0.3,
#ambient=0.5,
# label_size=(0.4, 0.16),
# )

# plotter.subplot(3, 0) # TODO draw in the 4th box
# # TODO Draw Gaussian points
# plotter.add_mesh(pv.PolyData(mesh.points),scalars=node_data, cmap='coolwarm',
# scalar_bar_args={<!-- -->
# 'title': 'Temperature',
# "vertical": "False",
# "height": 0.65,
# "position_x": 0.85,
# "position_y": 0.1},
# show_edges=False)


def fun_pick(point):
    points = mesh.points
    # Calculate the distance between the point and each point in the array
    distances = np.linalg.norm(points - point, axis=1)
    # Find the index of the closest point
    index = np.argmin(distances)
    plotter.add_point_labels(np.array([points[index]]), np.array([node_data[index]]),
                             point_color='black',
                             point_size=10,
                             font_size=20,
                             name='point_1' # TODO The name of the participant added for easy updating. If an actor of this name already exists in the presentation window, it will be replaced by the new actor.
                             )

# TODO pick point
plotter.enable_point_picking(callback=fun_pick, # TODO callback function, the first parameter is the coordinates of the clicked point [-33.262 -27.3781 -22.936]
        tolerance=0.025, # TODO picking error, according to screen percentage
        left_clicking=False, # TODO defaults to right mouse button selection, True means left mouse button selection
        picker=PickerType.POINT, # TODO selected type hardware, cell, point, volume
        show_message=True, # TODO Show messages about how to use the point picking tool. If this is a string, that will be the message displayed.
        font_size=18, # TODO Set the size of the message.
        color='black', # TODO Display the selected color.
        point_size=10, # TODO If ' show_point ' ' is ' True', the size of the selected point.
        show_point=True, # TODO displays the selected point after clicking.
        use_picker=False, # TODO When ' ' True ' ', the callback function will also be passed to the selector.
        pickable_window=False, # TODO When "True" and the selected picker supports it, points in the 3D window are pickable.
        clear_on_no_selection=True, # TODO Clear the selection when no points are selected.
        # **kwargs, #TODO
        )






cent = np.random.random((1, 3))
direction = np.random.random((1, 3))
print(cent)
print(direction)
# _ = plotter.add_arrows(cent, direction, mag=1) # TODO arrows

_ = plotter.add_axes( # TODO Lower left corner coordinate axis
    line_width=5,
    cone_radius=0.6,
    shaft_length=0.7,
    tip_length=0.3,
    ambient=0.5, ambient
    label_size=(0.4, 0.16),
)
# _ = plotter.add_axes_at_origin() # TODO add coordinate axis at the center position

_ = plotter.add_camera_orientation_widget() # TODO The coordinate axis in the upper right corner has a negative axis

chart = pv.Chart2D() # TODO chart
_ = chart.plot(range(10), range(10))
# plotter.add_chart(chart)

# mesh = pyvista.Sphere()
# actor = plotter.add_mesh(mesh)
# def toggle_vis(flag):
# actor.SetVisibility(flag)
# _ = plotter.add_checkbox_button_widget(toggle_vis, value=True) # TODO Add a checkbox CheckBox in the lower left corner


# _ = plotter.add_cursor() # TODO Add cursor, frame it, not mouse

# _ = plotter.add_legend(bcolor='w', face=None)

plotter.show()