CurvatureBandsWithGlyphs
Repository source: CurvatureBandsWithGlyphs
Description¶
In this example we are coloring the surface by partitioning either the gaussian or mean curvature into bands and using arrows to display the normals on the surface.
Rather beautiful surfaces are generated.
The banded contour filter and a categorical lookup table are used to generate the curvature bands on the surface. To further enhance the surface, the surface normals are glyphed and colored by elevation using an ordinal lookup table.
The scalar bar widgets vary is size reflecting the number of bands being used.
Note that:
-
If the regions on a surface have zero Gaussian curvature, then they can be flattened into a plane with no distortion, and the geometry of the region is Euclidean geometry.
-
If the regions on a surface have positive Gaussian curvature, then the geometry of the surface is spherical geometry.
-
If the regions on the surface have a negative Gaussian curvature, then the geometry of the surface is hyperbolic geometry.
In the above image you can see that the parametric hills incorporate all of these geometries.
The surface selected is the parametric hills surface. The problem with the parametric hills surface is:
- Most of the gaussian curvatures will lie in the range -1 to 0.2 (say) with a few large values say 20 to 40 at the peaks of the hills.
- The edges of the random hills surface also have large irregular values so we need to handle these also. In order to fix this, a function is provided to adjust the edges.
So we need to manually generate custom bands to group the curvatures. The bands selected in the examples show that the surface is mostly planar with some hyperbolic regions (saddle points) and some spherical regions. We also generate custom bands for the elevation, these are reflected in the arrow glyphs.
Feel free to experiment with different color schemes and/or the other sources from the parametric function group or the torus etc. Choose color schemes from ColorSeriesPatches. Make sure that the number of bands used in your surface matches the number of colors in the color series patches that you select.
You will usually need to adjust the parameters for maskPts, arrow and glyph for a nice appearance.
A histogram of the frequencies can also be output to the console. This is useful if you want to get an idea of the distribution of the scalars in each band.
Question
If you have a question about this example, please use the VTK Discourse Forum
Code¶
CurvatureBandsWithGlyphs.py
#!/usr/bin/env python
import copy
import math
from dataclasses import dataclass
import numpy as np
# noinspection PyUnresolvedReferences
import vtkmodules.vtkRenderingOpenGL2
from vtk.util import numpy_support
from vtkmodules.numpy_interface import dataset_adapter as dsa
from vtkmodules.vtkCommonColor import (
vtkColorSeries,
vtkNamedColors
)
from vtkmodules.vtkCommonComputationalGeometry import (
vtkParametricRandomHills,
vtkParametricTorus
)
from vtkmodules.vtkCommonCore import (
VTK_DOUBLE,
vtkDoubleArray,
vtkFloatArray,
vtkIdList,
vtkLookupTable,
vtkPoints,
vtkVariant,
vtkVariantArray
)
from vtkmodules.vtkCommonDataModel import vtkPolyData
from vtkmodules.vtkCommonTransforms import vtkTransform
from vtkmodules.vtkFiltersCore import (
vtkCleanPolyData,
vtkDelaunay2D,
vtkElevationFilter,
vtkFeatureEdges,
vtkGlyph3D,
vtkGenerateIds,
vtkMaskPoints,
vtkPolyDataNormals,
vtkPolyDataTangents,
vtkReverseSense,
vtkTriangleFilter
)
from vtkmodules.vtkFiltersGeneral import (
vtkCurvatures,
vtkTransformFilter
)
from vtkmodules.vtkFiltersModeling import vtkBandedPolyDataContourFilter
from vtkmodules.vtkFiltersSources import (
vtkArrowSource,
vtkParametricFunctionSource,
vtkPlaneSource,
vtkSphereSource,
vtkSuperquadricSource
)
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera
from vtkmodules.vtkInteractionWidgets import (
vtkCameraOrientationWidget,
vtkOrientationMarkerWidget,
vtkScalarBarRepresentation,
vtkScalarBarWidget,
vtkTextRepresentation,
vtkTextWidget
)
from vtkmodules.vtkRenderingAnnotation import (
vtkAxesActor,
vtkScalarBarActor
)
from vtkmodules.vtkRenderingCore import (
vtkActor,
vtkPolyDataMapper,
vtkRenderWindow,
vtkRenderWindowInteractor,
vtkRenderer,
vtkTextActor,
vtkTextProperty
)
def get_program_parameters():
import argparse
description = 'Color a surface using curvatures, adding normal vectors colored by elevation.'
epilogue = '''
For example: "parametric hills" -f
Will display the surface colored by curvature along with surface normals.
'''
parser = argparse.ArgumentParser(description=description, epilog=epilogue,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('surface_name', nargs='?', default='parametric hills',
help='The name of the surface - enclose the name in quotes if it has spaces.')
parser.add_argument('-m', '--mean_curvature', action='store_true', help='Display the frequency table.')
parser.add_argument('-f', '--frequency_table', action='store_true', help='Display the frequency table.')
parser.add_argument('-o', '--omw', action='store_true',
help='Use an OrientationMarkerWidget instead of a CameraOrientationWidget.')
args = parser.parse_args()
return args.surface_name, args.mean_curvature, args.frequency_table, args.omw
def main(argv):
# ------------------------------------------------------------
# Create the surface, lookup tables, contour filter etc.
# ------------------------------------------------------------
surface_name, mean_curvature, frequency_table, use_omw = get_program_parameters()
available_surfaces = ['hills', 'parametric hills', 'parametric torus', 'plane', 'sphere', 'torus']
# Surfaces whose curvatures need to be adjusted along the edges of the surface or constrained.
needs_adjusting = ['hills', 'parametric hills', 'parametric torus', 'plane']
surface_name = ' '.join(surface_name.lower().replace('_', ' ').split())
if surface_name in ['parametrichills', 'random hills', 'randomhills']:
surface_name = 'parametric hills'
if surface_name == 'parametrictorus':
surface_name = 'parametric torus'
if surface_name.lower() not in available_surfaces:
print('Nonexistent surface:', surface_name)
print('Available surfaces are:')
asl = sorted(available_surfaces)
asl = [asl[i].title() for i in range(0, len(asl))]
asl = [asl[i:i + 5] for i in range(0, len(asl), 5)]
for i in range(0, len(asl)):
s = ', '.join(asl[i])
if i < len(asl) - 1:
s += ','
print(f' {s}')
print('If a name has spaces in it, delineate the name with quotes e.g. "parametric hills"')
return
source = get_source(surface_name)
if not source:
print('The surface is not available.')
return
cc = vtkCurvatures(input_data=source)
if mean_curvature:
cc.SetCurvatureTypeToMean()
cc.update()
curvature = cc.output.point_data.scalars.name
if surface_name in needs_adjusting:
adjust_edge_curvatures(cc.output, curvature)
if surface_name == 'plane':
constrain_curvatures(cc.output, curvature, 0.0, 0.0)
if surface_name == 'sphere':
# The sphere radius is 1.
# Mean curvature is 1/r
constrain_curvatures(cc.output, curvature, 1.0, 1.0)
else:
cc.SetCurvatureTypeToGaussian()
cc.update()
curvature = cc.output.point_data.scalars.name
if surface_name in needs_adjusting:
adjust_edge_curvatures(cc.output, curvature)
if surface_name == 'plane':
constrain_curvatures(cc.output, curvature, 0.0, 0.0)
if surface_name == 'sphere':
# The sphere radius is 1.
# Gaussian curvature is 1/r^2
constrain_curvatures(cc.output, curvature, 1.0, 1.0)
curv_bg = get_curvature_glyphs(surface_name, cc, curvature,
precision=10, frequency_table=frequency_table,
nearest_integer=False)
elev_bg = get_elevation_glyphs(surface_name, source,
precision=10, frequency_table=frequency_table,
nearest_integer=False)
# ------------------------------------------------------------
# Create the mappers and actors
# ------------------------------------------------------------
colors = vtkNamedColors()
src_mapper = vtkPolyDataMapper(input_connection=curv_bg.bcf.output_port,
lookup_table=curv_bg.lut,
scalar_range=curv_bg.scalar_range,
scalar_mode=Mapper.ScalarMode.VTK_SCALAR_MODE_USE_CELL_DATA)
src_actor = vtkActor(mapper=src_mapper)
# Create contour edges
edge_mapper = vtkPolyDataMapper(input_data=curv_bg.bcf.contour_edges_output,
resolve_coincident_topology=Mapper.ResolveCoincidentTopology.VTK_RESOLVE_POLYGON_OFFSET)
edge_actor = vtkActor(mapper=edge_mapper)
edge_actor.property.color = colors.GetColor3d('Black')
glyph_mapper = vtkPolyDataMapper(input_connection=elev_bg.glyphs.output_port,
lookup_table=elev_bg.lut1,
scalar_range=elev_bg.scalar_range,
color_mode=Mapper.ColorMode.VTK_COLOR_MODE_MAP_SCALARS,
scalar_visibility=True,
scalar_mode=Mapper.ScalarMode.VTK_SCALAR_MODE_USE_POINT_FIELD_DATA)
glyph_mapper.SelectColorArray('Elevation')
glyph_actor = vtkActor(mapper=glyph_mapper)
window_width = 800
window_height = 800
# ------------------------------------------------------------
# Create the RenderWindow, Renderer and Interactor
# ------------------------------------------------------------
ren = vtkRenderer(background=colors.GetColor3d('ParaViewBlueGrayBkg'))
ren_win = vtkRenderWindow(size=(window_width, window_height),
window_name='CurvatureBandsWithGlyphs')
ren_win.AddRenderer(ren)
iren = vtkRenderWindowInteractor()
iren.render_window = ren_win
style = vtkInteractorStyleTrackballCamera()
iren.interactor_style = style
# Add actors.
ren.AddViewProp(src_actor)
ren.AddViewProp(edge_actor)
ren.AddViewProp(glyph_actor)
# Position the source name according to its length.
text_positions = get_text_positions(available_surfaces,
justification=TextProperty.Justification.VTK_TEXT_LEFT,
vertical_justification=TextProperty.VerticalJustification.VTK_TEXT_TOP,
width=0.25)
title_text_property = vtkTextProperty(color=colors.GetColor3d('AliceBlue'), bold=True, italic=True, shadow=True,
font_size=12,
justification=TextProperty.Justification.VTK_TEXT_LEFT)
label_text_property = vtkTextProperty(color=colors.GetColor3d('AliceBlue'), bold=False, italic=False, shadow=True,
font_size=12,
justification=TextProperty.Justification.VTK_TEXT_LEFT)
text_actor = vtkTextActor(input=surface_name.title(), text_scale_mode=vtkTextActor.TEXT_SCALE_MODE_NONE,
text_property=title_text_property)
# Create the text representation. Used for positioning the text actor.
text_representation = vtkTextRepresentation(enforce_normalized_viewport_bounds=True)
text_representation.position_coordinate.value = text_positions[surface_name]['p']
text_representation.position2_coordinate.value = text_positions[surface_name]['p2']
text_widget = vtkTextWidget(representation=text_representation, text_actor=text_actor,
default_renderer=ren, interactor=iren,
selectable=False, enabled=True)
curv_sbp = ScalarBarProperties()
curv_sbp.title_text = curvature.replace('_', ' ') + '\n'
curv_sbp.number_of_labels = len(curv_bg.labels)
# lut puts the lowest value at the top of the scalar bar.
# lutr puts the highest value at the top of the scalar bar.
curv_sbp.lut = curv_bg.lut
curv_sbp.orientation = False
max_bands = 9
if surface_name == 'hills':
curv_sbp.position_h = position_sbw_h(9, max_bands)
elif surface_name == 'parametric hills':
curv_sbp.position_h = position_sbw_h(7, max_bands)
elif surface_name in ['plane', 'sphere']:
curv_sbp.position_h = position_sbw_h(1, max_bands)
else:
curv_sbp.position_h = position_sbw_h(5, max_bands)
curv_sb_widget = make_scalar_bar_widget(curv_sbp, title_text_property,
label_text_property, ren, iren)
elev_sbp = ScalarBarProperties()
elev_sbp.title_text = 'Elevation\n'
elev_sbp.number_of_labels = len(elev_bg.labels)
elev_sbp.lut = elev_bg.lutr
elev_sbp.orientation = True
max_bands = 8
if surface_name in ['hills', 'parametric hills']:
elev_sbp.position_v = position_sbw_v(8, max_bands)
elif surface_name == 'plane':
elev_sbp.position_v = position_sbw_v(1, max_bands)
else:
elev_sbp.position_v = position_sbw_v(5, max_bands)
elev_sb_widget = make_scalar_bar_widget(elev_sbp, title_text_property,
label_text_property, ren, iren)
# Important: The interactor must be set prior to enabling the widgets.
if use_omw:
rgb = [0.0] * 4
colors.GetColor("Carrot", rgb)
rgb = tuple(rgb[:3])
widget = vtkOrientationMarkerWidget(orientation_marker=vtkAxesActor(),
interactor=iren, default_renderer=ren,
outline_color=rgb, viewport=(0.8, 0.8, 1.0, 1.0), zoom=1.0, enabled=True,
interactive=True)
else:
cow = vtkCameraOrientationWidget(parent_renderer=ren)
# Enable the widget.
cow.On()
ren.ResetCamera()
adjust_camera_parameters(surface_name, ren)
ren_win.Render()
iren.Start()
def adjust_edge_curvatures(source, curvature_name, epsilon=1.0e-08):
"""
This function adjusts curvatures along the edges of the surface by replacing
the value with the average value of the curvatures of points in the neighborhood.
Remember to update the vtkCurvatures object before calling this.
:param source: A vtkPolyData object corresponding to the vtkCurvatures object.
:param curvature_name: The name of the curvature, 'Gauss_Curvature' or 'Mean_Curvature'.
:param epsilon: Absolute curvature values less than this will be set to zero.
:return: The vtkPolyData object with the adjusted edge curvatures.
"""
def point_neighbourhood(pt_id):
"""
Find the ids of the neighbors of pt_id.
:param pt_id: The point id.
:return: The neighbour ids.
"""
"""
Extract the topological neighbors for point pId. In two steps:
1) source.GetPointCells(pt_id, cell_ids)
2) source.GetCellPoints(cell_id, cell_point_ids) for all cell_id in cell_ids
"""
cell_ids = vtkIdList()
source.GetPointCells(pt_id, cell_ids)
neighbour = set()
for cell_idx in range(0, cell_ids.GetNumberOfIds()):
cell_id = cell_ids.GetId(cell_idx)
cell_point_ids = vtkIdList()
source.GetCellPoints(cell_id, cell_point_ids)
for cell_pt_idx in range(0, cell_point_ids.GetNumberOfIds()):
neighbour.add(cell_point_ids.GetId(cell_pt_idx))
return neighbour
def compute_distance(pt_id_a, pt_id_b):
"""
Compute the distance between two points given their ids.
:param pt_id_a:
:param pt_id_b:
:return:
"""
pt_a = np.array(source.GetPoint(pt_id_a))
pt_b = np.array(source.GetPoint(pt_id_b))
return np.linalg.norm(pt_a - pt_b)
# Get the active scalars
source.GetPointData().SetActiveScalars(curvature_name)
np_source = dsa.WrapDataObject(source)
curvatures = np_source.PointData[curvature_name]
# Get the boundary point IDs.
array_name = 'ids'
id_filter = vtkGenerateIds(input_data=source, point_ids=True, cell_ids=False,
point_ids_array_name=array_name, cell_ids_array_name=array_name)
edges = vtkFeatureEdges(boundary_edges=True, manifold_edges=False,
non_manifold_edges=False, feature_edges=False)
(source >> id_filter >> edges).update()
edge_array = edges.output.GetPointData().GetArray(array_name)
boundary_ids = []
for i in range(edges.output.GetNumberOfPoints()):
boundary_ids.append(edge_array.GetValue(i))
# Remove duplicate Ids.
p_ids_set = set(boundary_ids)
# Iterate over the edge points and compute the curvature as the weighted
# average of the neighbours.
count_invalid = 0
for p_id in boundary_ids:
p_ids_neighbors = point_neighbourhood(p_id)
# Keep only interior points.
p_ids_neighbors -= p_ids_set
# Compute distances and extract curvature values.
curvs = [curvatures[p_id_n] for p_id_n in p_ids_neighbors]
dists = [compute_distance(p_id_n, p_id) for p_id_n in p_ids_neighbors]
curvs = np.array(curvs)
dists = np.array(dists)
curvs = curvs[dists > 0]
dists = dists[dists > 0]
if len(curvs) > 0:
weights = 1 / np.array(dists)
weights /= weights.sum()
new_curv = np.dot(curvs, weights)
else:
# Corner case.
count_invalid += 1
# Assuming the curvature of the point is planar.
new_curv = 0.0
# Set the new curvature value.
curvatures[p_id] = new_curv
# Set small values to zero.
if epsilon != 0.0:
curvatures = np.where(abs(curvatures) < epsilon, 0, curvatures)
# Curvatures is now an ndarray
curv = numpy_support.numpy_to_vtk(num_array=curvatures.ravel(),
deep=True,
array_type=VTK_DOUBLE)
curv.SetName(curvature_name)
source.GetPointData().RemoveArray(curvature_name)
source.GetPointData().AddArray(curv)
source.GetPointData().SetActiveScalars(curvature_name)
def constrain_curvatures(source, curvature_name, lower_bound=0.0, upper_bound=0.0):
"""
This function constrains curvatures to the range [lower_bound ... upper_bound].
Remember to update the vtkCurvatures object before calling this.
:param source: A vtkPolyData object corresponding to the vtkCurvatures object.
:param curvature_name: The name of the curvature, 'Gauss_Curvature' or 'Mean_Curvature'.
:param lower_bound: The lower bound.
:param upper_bound: The upper bound.
:return: The vtkPolyData object with the constrained curvatures.
"""
bounds = list()
if lower_bound < upper_bound:
bounds.append(lower_bound)
bounds.append(upper_bound)
else:
bounds.append(upper_bound)
bounds.append(lower_bound)
# Get the active scalars.
source.GetPointData().SetActiveScalars(curvature_name)
np_source = dsa.WrapDataObject(source)
curvatures = np_source.PointData[curvature_name]
# Set upper and lower bounds.
curvatures = np.where(curvatures < bounds[0], bounds[0], curvatures)
curvatures = np.where(curvatures > bounds[1], bounds[1], curvatures)
# Curvatures is now an ndarray
curv = numpy_support.numpy_to_vtk(num_array=curvatures.ravel(),
deep=True,
array_type=VTK_DOUBLE)
curv.SetName(curvature_name)
source.GetPointData().RemoveArray(curvature_name)
source.GetPointData().AddArray(curv)
source.GetPointData().SetActiveScalars(curvature_name)
def generate_elevations(src):
"""
Generate elevations over the surface.
:param: src - the vtkPolyData source.
:return: - vtkPolyData source with elevations.
"""
bounds = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
src.GetBounds(bounds)
if abs(bounds[2]) < 1.0e-8 and abs(bounds[3]) < 1.0e-8:
bounds[3] = bounds[2] + 1
elev_filter = vtkElevationFilter(input_data=src,
low_point=(0, bounds[2], 0),
high_point=(0, bounds[3], 0),
scalar_range=(bounds[2], bounds[3]))
elev_filter.update()
return elev_filter.GetPolyDataOutput()
def get_hills():
# Create four hills on a plane.
# This will have regions of negative, zero and positive Gaussian curvatures.
x_res = 50
y_res = 50
x_min = -5.0
x_max = 5.0
dx = (x_max - x_min) / (x_res - 1)
y_min = -5.0
y_max = 5.0
dy = (y_max - y_min) / (x_res - 1)
# Make a grid.
points = vtkPoints()
for i in range(0, x_res):
x = x_min + i * dx
for j in range(0, y_res):
y = y_min + j * dy
points.InsertNextPoint(x, y, 0)
# Add the grid points to a polydata object.
plane = vtkPolyData(points=points)
# Triangulate the grid.
delaunay = vtkDelaunay2D(input_data=plane)
polydata = delaunay.update().output
elevation = vtkDoubleArray(number_of_tuples=points.number_of_points)
# We define the parameters for the hills here.
# [[0: x0, 1: y0, 2: x variance, 3: y variance, 4: amplitude]...]
hd = [[-2.5, -2.5, 2.5, 6.5, 3.5], [2.5, 2.5, 2.5, 2.5, 2],
[5.0, -2.5, 1.5, 1.5, 2.5], [-5.0, 5, 2.5, 3.0, 3]]
xx = [0.0] * 2
for i in range(0, points.number_of_points):
x = list(polydata.GetPoint(i))
for j in range(0, len(hd)):
xx[0] = (x[0] - hd[j][0] / hd[j][2]) ** 2.0
xx[1] = (x[1] - hd[j][1] / hd[j][3]) ** 2.0
x[2] += hd[j][4] * math.exp(-(xx[0] + xx[1]) / 2.0)
polydata.GetPoints().SetPoint(i, x)
elevation.SetValue(i, x[2])
textures = vtkFloatArray(name='Textures', number_of_components=2, number_of_tuples=2 * polydata.number_of_points)
for i in range(0, x_res):
tc = [i / (x_res - 1.0), 0.0]
for j in range(0, y_res):
# tc[1] = 1.0 - j / (y_res - 1.0)
tc[1] = j / (y_res - 1.0)
textures.SetTuple(i * y_res + j, tc)
polydata.GetPointData().SetScalars(elevation)
polydata.GetPointData().GetScalars().name = 'Elevation'
polydata.GetPointData().TCoords = textures
normals = vtkPolyDataNormals(feature_angle=30, splitting=False)
tr = vtkTransform()
tr.RotateX(-90)
tf = vtkTransformFilter(transform=tr)
return (polydata >> normals >> tf).update().output
def get_parametric_hills():
"""
Make a parametric hills surface as the source.
:return: vtkPolyData with normal and scalar data.
"""
random_seed = 1
number_of_hills = 30
# If you want a plane
# hill_amplitude=0
fn = vtkParametricRandomHills(random_seed=random_seed, number_of_hills=number_of_hills)
fn.AllowRandomGenerationOn()
u_resolution = 50
v_resolution = 50
source = vtkParametricFunctionSource(parametric_function=fn,
u_resolution=u_resolution, v_resolution=v_resolution,
generate_texture_coordinates=True)
source.SetScalarModeToZ()
source.update()
# Rename the scalars to 'Elevation' since we are using the Z-scalars as elevations.
source.output.GetPointData().GetScalars().SetName('Elevation')
# Build the tangents.
tangents = vtkPolyDataTangents()
tr = vtkTransform()
tr.Translate(0.0, 5.0, 15.0)
tr.RotateX(-90.0)
tf = vtkTransformFilter(transform=tr)
return (source >> tangents >> tf).update().output
def get_parametric_torus():
"""
Make a parametric torus as the source.
:return: vtkPolyData with normal and scalar data.
"""
fn = vtkParametricTorus(ring_radius=5, cross_section_radius=2)
source = vtkParametricFunctionSource(parametric_function=fn,
u_resolution=50, v_resolution=50,
generate_texture_coordinates=True)
source.SetScalarModeToZ()
source.update()
# Rename the scalars to 'Elevation' since we are using the Z-scalars as elevations.
source.output.GetPointData().GetScalars().SetName('Elevation')
# Build the tangents.
tangents = vtkPolyDataTangents()
tr = vtkTransform()
# transform.Translate(0.0, 0.0, 0.0)
tr.RotateX(-90.0)
t = vtkTransformFilter(transform=tr)
return (source >> tangents >> t).update().output
def get_plane():
"""
Make a plane as the source.
:return: vtkPolyData with normal and scalar data.
"""
source = vtkPlaneSource(origin=(-10.0, -10.0, 0.0),
point2=(-10.0, 10.0, 0.0), point1=(10.0, -10.0, 0.0),
x_resolution=10, y_resolution=10)
tr = vtkTransform()
tr.Translate(0.0, 0.0, 0.0)
tr.RotateX(-90.0)
tf = vtkTransformFilter(transform=tr)
# We have a m x n array of quadrilaterals arranged as a regular tiling in a
# plane. So pass it through a triangle filter since the curvature filter only
# operates on polys.
tri = vtkTriangleFilter()
# Pass it though a CleanPolyDataFilter and merge any points which
# are coincident, or very close
cleaner = vtkCleanPolyData(tolerance=0.005)
return (source >> tf >> tri >> cleaner).update().output
def get_sphere():
source = vtkSphereSource(center=(0.0, 0.0, 0.0), radius=1.0,
theta_resolution=32, phi_resolution=32)
return source.update().output
def get_torus():
"""
Make a torus as the source.
:return: vtkPolyData with normal and scalar data.
"""
source = vtkSuperquadricSource(center=(0.0, 0.0, 0.0), scale=(1.0, 1.0, 1.0),
phi_resolution=64,
theta_resolution=64, theta_roundness=1,
thickness=0.5, size=10, toroidal=True)
# The quadric is made of strips, so pass it through a triangle filter as
# the curvature filter only operates on polys
tri = vtkTriangleFilter()
# The quadric has nasty discontinuities from the way the edges are generated
# so let's pass it though a CleanPolyDataFilter and merge any points which
# are coincident, or very close
cleaner = vtkCleanPolyData(tolerance=0.005)
return (source >> tri >> cleaner).update().output
def get_source(source):
surface = source.lower()
if surface == 'hills':
return get_hills()
elif surface == 'parametric hills':
return get_parametric_hills()
elif surface == 'parametric torus':
return get_parametric_torus()
elif surface == 'plane':
return generate_elevations(get_plane())
elif surface == 'sphere':
return generate_elevations(get_sphere())
elif surface == 'torus':
return generate_elevations(get_torus())
print('The surface is not available.')
print('Using parametric hills instead.')
return get_parametric_hills()
def reverse_lut(lut):
"""
Create a lookup table with the colors reversed.
:param: lut - An indexed lookup table.
:return: The reversed indexed lookup table.
"""
lutr = vtkLookupTable()
lutr.DeepCopy(lut)
t = lut.number_of_table_values - 1
rev_range = reversed(list(range(t + 1)))
for i in rev_range:
rgba = [0.0] * 3
v = float(i)
lut.GetColor(v, rgba)
rgba.append(lut.GetOpacity(v))
lutr.SetTableValue(t - i, rgba)
t = lut.number_of_annotated_values - 1
rev_range = reversed(list(range(t + 1)))
for i in rev_range:
lutr.SetAnnotation(t - i, lut.GetAnnotation(i))
return lutr
def get_glyphs(src, scale_factor=1.0, reverse_normals=False):
"""
Glyph the normals on the surface.
You may need to adjust the parameters for mask_pts, arrow and glyph for a
nice appearance.
:param: src - the surface to glyph.
:param: reverse_normals - if True the normals on the surface are reversed.
:return: The glyph object.
"""
if reverse_normals:
# Sometimes the contouring algorithm can create a volume whose gradient
# vector and ordering of polygon (using the right hand rule) are
# inconsistent. vtkReverseSense cures this problem.
reverse = vtkReverseSense(reverse_cells=True, reverse_normals=True)
# Choose a random subset of points.
mask_pts = vtkMaskPoints(on_ratio=5, random_mode=True)
src >> reverse >> mask_pts
else:
# Choose a random subset of points.
mask_pts = vtkMaskPoints(on_ratio=5, random_mode=True)
src >> mask_pts
# Source for the glyph filter
arrow = vtkArrowSource(tip_resolution=16, tip_length=0.3, tip_radius=0.1)
# glyph = vtkGlyph3D()
glyphs = vtkGlyph3D(source_connection=arrow.output_port,
input_connection=mask_pts.output_port,
scaling=True, scale_mode=Glyph3D.ScaleMode.VTK_SCALE_BY_VECTOR,
scale_factor=scale_factor, orient=True, clamping=False,
vector_mode=Glyph3D.VectorMode.VTK_USE_NORMAL,
color_mode=Glyph3D.ColorMode.VTK_COLOR_BY_VECTOR)
return glyphs
def get_bands(scalar_range, number_of_bands, precision=2, nearest_integer=False):
"""
Divide a range into bands
:param: scalar_range - [min, max] the range that is to be covered by the bands.
:param: number_of_bands - The number of bands, a positive integer.
:param: precision - The decimal precision of the bounds.
:param: nearest_integer - If True then [floor(min), ceil(max)] is used.
:return: A dictionary consisting of the band number and [min, midpoint, max] for each band.
"""
prec = abs(precision)
if prec > 14:
prec = 14
bands = dict()
if (scalar_range[1] < scalar_range[0]) or (number_of_bands <= 0):
return bands
x = list(scalar_range)
if nearest_integer:
x[0] = math.floor(x[0])
x[1] = math.ceil(x[1])
dx = (x[1] - x[0]) / float(number_of_bands)
b = [x[0], x[0] + dx / 2.0, x[0] + dx]
i = 0
while i < number_of_bands:
b = list(map(lambda ele_b: round(ele_b, prec), b))
if i == 0:
b[0] = x[0]
bands[i] = b
b = [b[0] + dx, b[1] + dx, b[2] + dx]
i += 1
return bands
def get_custom_bands(scalar_range, number_of_bands, my_bands):
"""
Divide a range into custom bands.
You need to specify each band as a list [r1, r2] where r1 < r2 and append these to a list.
The list should ultimately look like this: [[r1, r2], [r2, r3], [r3, r4]...]
:param: scalar_range - [min, max] the range that is to be covered by the bands.
:param: number_of_bands - the number of bands, a positive integer.
:return: A dictionary consisting of band number and [min, midpoint, max] for each band.
"""
bands = dict()
if (scalar_range[1] < scalar_range[0]) or (number_of_bands <= 0):
return bands
x = my_bands
# Determine the index of the range minimum and range maximum.
idx_min = 0
for idx in range(0, len(my_bands)):
if my_bands[idx][1] > scalar_range[0] >= my_bands[idx][0]:
idx_min = idx
break
idx_max = len(my_bands) - 1
for idx in range(len(my_bands) - 1, -1, -1):
if my_bands[idx][1] > scalar_range[1] >= my_bands[idx][0]:
idx_max = idx
break
# Set the minimum to match the range minimum.
x[idx_min][0] = scalar_range[0]
x[idx_max][1] = scalar_range[1]
x = x[idx_min: idx_max + 1]
for idx, e in enumerate(x):
bands[idx] = [e[0], e[0] + (e[1] - e[0]) / 2, e[1]]
return bands
def get_frequencies(bands, src):
"""
Count the number of scalars in each band.
The scalars used are the active scalars in the polydata.
:param: bands - The bands.
:param: src - The vtkPolyData source.
:return: The frequencies of the scalars in each band.
"""
freq = dict()
for i in range(len(bands)):
freq[i] = 0
tuples = src.GetPointData().GetScalars().GetNumberOfTuples()
for i in range(tuples):
x = src.GetPointData().GetScalars().GetTuple1(i)
for j in range(len(bands)):
if x <= bands[j][2]:
freq[j] += 1
break
return freq
def adjust_ranges(bands, freq):
"""
The bands and frequencies are adjusted so that the first and last
frequencies in the range are non-zero.
:param bands: The dictionary containing the bands.
:param freq: The frequency dictionary.
:return: Adjusted bands and frequencies.
"""
# Get the indices of the first and last non-zero elements.
first = 0
for k, v in freq.items():
if v != 0:
first = k
break
rev_keys = list(freq.keys())[::-1]
last = rev_keys[0]
for idx in list(freq.keys())[::-1]:
if freq[idx] != 0:
last = idx
break
# Now adjust the ranges.
min_key = min(freq.keys())
max_key = max(freq.keys())
for idx in range(min_key, first):
freq.pop(idx)
bands.pop(idx)
for idx in range(last + 1, max_key + 1):
freq.popitem()
bands.popitem()
old_keys = freq.keys()
adj_freq = dict()
adj_bands = dict()
for idx, k in enumerate(old_keys):
adj_freq[idx] = freq[k]
adj_bands[idx] = bands[k]
return adj_bands, adj_freq
def get_curvature_glyphs(surface_name, source, curvature,
precision, frequency_table=False, nearest_integer=False):
"""
Get curvature glyphs and the corresponding banded polydata filter for the surface.
:param: surface_name - the name of the surface.
:param: src - the polydata surface to glyph.
:param: curvature - The curvature type: Gauss_Curvature or Mean_Curvature
:param: precision - the precision level.
:param: frequency_table - True if you want a frequency table printed.
:param: nearest_integer - If True use the nearest integer when generating the bands.
:return: A dataclass holding glyphs, bcf, lut, lutr, lut1, lut1r, scalar_range, labels
"""
# The length of the normal arrow glyphs.
scale_factor = 1.0
if surface_name == 'hills':
scale_factor = 0.5
elif surface_name == 'sphere':
scale_factor = 0.2
source.output.GetPointData().SetActiveScalars(curvature)
scalar_range = source.output.point_data.GetScalars(curvature).range
color_series = vtkColorSeries()
if surface_name == 'parametric hills':
color_series.color_scheme = color_series.BREWER_DIVERGING_SPECTRAL_7
elif surface_name == 'hills':
color_series.color_scheme = color_series.BREWER_DIVERGING_SPECTRAL_9
elif surface_name in ['plane', 'sphere']:
color_series.color_scheme = color_series.CITRUS
else:
color_series.color_scheme = color_series.BREWER_DIVERGING_SPECTRAL_5
lut = vtkLookupTable()
color_series.BuildLookupTable(lut, color_series.CATEGORICAL)
lut.SetNanColor(0, 0, 0, 1)
lut.SetTableRange(scalar_range)
lut1 = vtkLookupTable()
color_series.BuildLookupTable(lut1, color_series.ORDINAL)
lut1.SetNanColor(0, 0, 0, 1)
lut1.SetTableRange(scalar_range)
number_of_bands = lut.number_of_table_values
lut1.number_of_table_values = number_of_bands
bands = get_bands(scalar_range, number_of_bands, precision, nearest_integer)
if curvature == 'Gauss_Curvature':
if surface_name == 'parametric hills':
# These are my custom bands.
# Generated by first running:
# bands = get_bands(scalar_range_curvatures, number_of_bands, False)
# then:
# freq = frequencies(bands, src)
# print_bands_frequencies(bands, freq)
# Finally using the output to create this table:
# my_bands = [
# [-0.630, -0.190], [-0.190, -0.043], [-0.043, -0.0136],
# [-0.0136, 0.0158], [0.0158, 0.0452], [0.0452, 0.0746],
# [0.0746, 0.104], [0.104, 0.251], [0.251, 1.131]]
# This demonstrates that the gaussian curvature of the surface
# is mostly planar with some hyperbolic regions (saddle points)
# and some spherical regions.
my_bands = [
[-0.630, -0.190], [-0.190, -0.043], [-0.043, 0.0452], [0.0452, 0.0746],
[0.0746, 0.104], [0.104, 0.251], [0.251, 1.131]]
# Comment this out if you want to see how allocating
# equally spaced bands works.
bands = get_custom_bands(scalar_range, number_of_bands, my_bands)
elif surface_name == 'hills':
my_bands = [
[-2.104, -0.15], [-0.15, -0.1], [-0.1, -0.05],
[-0.05, -0.02], [-0.02, -0.005], [-0.005, -0.0005],
[-0.0005, 0.0005], [0.0005, 0.09], [0.09, 4.972]]
# Comment this out if you want to see how allocating
# equally spaced bands works.
bands = get_custom_bands(scalar_range, number_of_bands, my_bands)
# Adjust the number of table values and scalar range.
scalar_range = (bands[0][0], bands[len(bands) - 1][2])
lut.TableRange = scalar_range
lut.number_of_table_values = len(bands)
lut1.TableRange = scalar_range
lut1.number_of_table_values = len(bands)
if frequency_table:
print(f'{surface_name.title()} {curvature}')
# The number of scalars in each band.
freq = get_frequencies(bands, source.output)
bands, freq = adjust_ranges(bands, freq)
print_bands_frequencies(bands, freq)
scalar_range = (bands[0][0], bands[len(bands) - 1][2])
lut.TableRange = scalar_range
lut.number_of_table_values = len(bands)
lut1.TableRange = scalar_range
lut1.number_of_table_values = len(bands)
# We will use the midpoint of the band as the label.
labels = []
for k in bands:
labels.append('{:4.2f}'.format(bands[k][1]))
# Annotate
values = vtkVariantArray()
for i in range(len(labels)):
values.InsertNextValue(vtkVariant(labels[i]))
for i in range(values.GetNumberOfTuples()):
lut.SetAnnotation(i, values.GetValue(i).ToString())
# Create the contour bands.
# We will use an indexed lookup table.
bcf = vtkBandedPolyDataContourFilter(input_data=source.output,
scalar_mode=BandedPolyDataContourFilter.ScalarMode.VTK_SCALAR_MODE_INDEX,
generate_contour_edges=True)
# Use either the minimum or maximum value for each band.
for i in range(len(bands)):
bcf.SetValue(i, bands[i][2])
glyphs = get_glyphs(source, scale_factor, reverse_normals=False)
bg = CurvatureBandedGlyphs
bg.glyphs = glyphs
bg.bcf = bcf
bg.lut = lut
bg.lutr = reverse_lut(lut)
bg.lut1 = lut1
bg.lut1r = reverse_lut(lut1)
bg.scalar_range = scalar_range
bg.labels = labels
return bg
@dataclass
class CurvatureBandedGlyphs:
glyphs: vtkGlyph3D
bcf: vtkBandedPolyDataContourFilter
lut: vtkLookupTable
lutr = vtkLookupTable
lut1: vtkLookupTable
lut1r: vtkLookupTable
scalar_range: tuple
labels: list
def get_elevation_glyphs(surface_name, source,
precision, frequency_table=False, nearest_integer=False):
"""
Get elevation glyphs and the corresponding banded polydata filter for the surface.
:param: surface_name - the name of the surface.
:param: src - the polydata surface to glyph.
:param: precision - the precision level.
:param: frequency_table - If true, display a frequency table corresponding to the bands.
:param: nearest_integer - If true, use the nearest integer when generating the bands.
:return: A dataclass holding glyphs, bcf, lut, lutr, lut1, lut1r scalar_range, labels
"""
# The length of the normal arrow glyphs.
scale_factor = 1.0
if surface_name == 'hills':
scale_factor = 0.5
elif surface_name == 'sphere':
scale_factor = 0.2
source.point_data.active_scalars = 'Elevation'
scalar_range = source.point_data.GetScalars('Elevation').range
color_series = vtkColorSeries()
if surface_name in ['hills', 'parametric hills']:
color_series.color_scheme = color_series.BREWER_DIVERGING_BROWN_BLUE_GREEN_8
else:
color_series.color_scheme = color_series.BREWER_DIVERGING_BROWN_BLUE_GREEN_5
lut = vtkLookupTable()
color_series.BuildLookupTable(lut, color_series.CATEGORICAL)
lut.SetNanColor(0, 0, 0, 1)
lut.SetTableRange(scalar_range)
lut1 = vtkLookupTable()
color_series.BuildLookupTable(lut1, color_series.ORDINAL)
lut1.SetNanColor(0, 0, 0, 1)
lut1.SetTableRange(scalar_range)
number_of_bands = lut.number_of_table_values
lut1.number_of_table_values = number_of_bands
bands = get_bands(scalar_range, number_of_bands, precision, nearest_integer)
if surface_name == 'parametric hills':
# These are my custom bands.
# Generated by first running:
# bands = get_bands(scalar_range, number_of_bands, precision, False)
# then:
# freq = get_frequencies(bands, source)
# print_bands_frequencies(bands, freq)
# Finally using the output to create this table:
my_bands = [
[0, 1.0], [1.0, 2.0], [2.0, 3.0],
[3.0, 4.0], [4.0, 5.0], [5.0, 6.0],
[6.0, 7.0], [7.0, 8.0]]
# Comment this out if you want to see how allocating
# equally spaced bands works.
bands = get_custom_bands(scalar_range, number_of_bands, my_bands)
# Adjust the number of table values and scalar range.
scalar_range = (bands[0][0], bands[len(bands) - 1][2])
lut.TableRange = scalar_range
lut.number_of_table_values = len(bands)
lut1.TableRange = scalar_range
lut1.number_of_table_values = len(bands)
if frequency_table:
print(f'{surface_name.title()} Elevation')
# The number of scalars in each band.
freq = get_frequencies(bands, source)
bands, freq = adjust_ranges(bands, freq)
print_bands_frequencies(bands, freq)
# We will use the midpoint of the band as the label.
labels = []
for k in bands:
labels.append('{:4.2f}'.format(bands[k][1]))
# Annotate
values = vtkVariantArray()
for i in range(len(labels)):
values.InsertNextValue(vtkVariant(labels[i]))
for i in range(values.GetNumberOfTuples()):
lut.SetAnnotation(i, values.GetValue(i).ToString())
# Create the contour bands.
# We will use an indexed lookup table.
bcf = vtkBandedPolyDataContourFilter(input_data=source,
scalar_mode=BandedPolyDataContourFilter.ScalarMode.VTK_SCALAR_MODE_INDEX,
generate_contour_edges=True)
# Use either the minimum or maximum value for each band.
for i in range(len(bands)):
bcf.SetValue(i, bands[i][2])
glyphs = get_glyphs(source, scale_factor, reverse_normals=False)
bg = ElevationBandedGlyphs
bg.glyphs = glyphs
bg.bcf = bcf
bg.lut = lut
bg.lutr = reverse_lut(lut)
bg.lut1 = lut1
bg.lut1r = reverse_lut(lut1)
bg.scalar_range = scalar_range
bg.labels = labels
return bg
@dataclass
class ElevationBandedGlyphs:
glyphs: vtkGlyph3D
bcf: vtkBandedPolyDataContourFilter
lut: vtkLookupTable
lutr = vtkLookupTable
lut1: vtkLookupTable
lut1r: vtkLookupTable
scalar_range: tuple
labels: list
class ScalarBarProperties:
"""
The properties needed for scalar bars.
"""
lut = None
# These are in pixels
maximum_dimensions = {'width': 100, 'height': 260}
title_text = '',
number_of_labels: int = 5
label_format = '{:0.2f}'
# Orientation vertical=True, horizontal=False.
orientation: bool = True
# Horizontal and vertical positioning.
# These are the default positions, don't change these.
default_v = {'p': (0.85, 0.05), 'p2': (0.1, 0.7)}
default_h = {'p': (0.125, 0.05), 'p2': (0.75, 0.1)}
# Modify these as needed.
position_v = copy.deepcopy(default_v)
position_h = copy.deepcopy(default_h)
def make_scalar_bar_widget(scalar_bar_properties, title_text_property, label_text_property, renderer,
interactor):
"""
Make a scalar bar widget.
:param scalar_bar_properties: The lookup table, title name, maximum dimensions in pixels and position.
:param title_text_property: The properties for the title.
:param label_text_property: The properties for the labels.
:param renderer: The default renderer.
:param interactor: The vtkInteractor.
:return: The scalar bar widget.
"""
sb_actor = vtkScalarBarActor(lookup_table=scalar_bar_properties.lut, title=scalar_bar_properties.title_text,
unconstrained_font_size=True,
number_of_labels=scalar_bar_properties.number_of_labels,
title_text_property=title_text_property, label_text_property=label_text_property,
label_format=scalar_bar_properties.label_format,
)
sb_rep = vtkScalarBarRepresentation(enforce_normalized_viewport_bounds=True,
orientation=scalar_bar_properties.orientation)
# Set the position.
sb_rep.position_coordinate.SetCoordinateSystemToNormalizedViewport()
sb_rep.position2_coordinate.SetCoordinateSystemToNormalizedViewport()
if scalar_bar_properties.orientation:
sb_rep.position_coordinate.value = scalar_bar_properties.position_v['p']
sb_rep.position2_coordinate.value = scalar_bar_properties.position_v['p2']
else:
sb_rep.position_coordinate.value = scalar_bar_properties.position_h['p']
sb_rep.position2_coordinate.value = scalar_bar_properties.position_h['p2']
widget = vtkScalarBarWidget(representation=sb_rep, scalar_bar_actor=sb_actor, default_renderer=renderer,
interactor=interactor, enabled=True)
return widget
def position_sbw_h(num_bands, max_bands):
"""
Position the vertical scalar bar widget.
:param: num_bands - the number of bands in the scalar bar.
:param: max_bands - the maximum number of bands.
:return: The scalar bar position.
"""
max_bands = abs(max_bands)
num_bands = abs(num_bands)
if num_bands > max_bands:
num_bands = max_bands
if num_bands == 0:
num_bands = 1
# Origin of the scalar bar.
xy0 = [0.125, 0.05]
# Width and height of the scalar bar.
dxy = [0.75, 0.1]
if num_bands >= max_bands:
return {'p': tuple(xy0), 'p2': tuple(dxy)}
dx = dxy[0] - xy0[0] * num_bands / max_bands
dxy[0] = dxy[0] * num_bands / max_bands
if num_bands == 1:
xy0[0] = 0.5 - dx * num_bands / (max_bands * 2)
else:
xy0[0] = 0.5 - dx * (num_bands + 1) / (max_bands * 2)
return {'p': tuple(xy0), 'p2': tuple(dxy)}
def position_sbw_v(num_bands, max_bands):
"""
Position the vertical scalar bar widget.
:param: num_bands - the number of bands in the scalar bar.
:param: max_bands - the maximum number of bands.
:return: The scalar bar position.
"""
max_bands = abs(max_bands)
num_bands = abs(num_bands)
if num_bands > max_bands:
num_bands = max_bands
if num_bands == 0:
num_bands = 1
# Origin of the scalar bar.
xy0 = [0.9, 0.25]
# Width and height of the scalar bar.
dxy = [0.08, 0.5]
if num_bands >= max_bands:
return {'p': tuple(xy0), 'p2': tuple(dxy)}
dy = dxy[1] - xy0[1] * num_bands / max_bands
dxy[1] = dxy[1] * num_bands / max_bands
if num_bands == 1:
xy0[1] = 0.5 - dy * num_bands / (max_bands * 2)
else:
xy0[1] = 0.5 - dy * (num_bands + 1) / (max_bands * 2)
return {'p': tuple(xy0), 'p2': tuple(dxy)}
def get_text_positions(names, justification=0, vertical_justification=0, width=0.96, height=0.1):
"""
Get viewport positioning information for a list of names.
:param names: The list of names.
:param justification: Horizontal justification of the text, default is left.
:param vertical_justification: Vertical justification of the text, default is bottom.
:param width: Width of the bounding_box of the text in screen coordinates.
:param height: Height of the bounding_box of the text in screen coordinates.
:return: A list of positioning information.
"""
# The gap between the left or right edge of the screen and the text.
dx = 0.02
width = abs(width)
if width > 0.96:
width = 0.96
y0 = 0.01
height = abs(height)
if height > 0.9:
height = 0.9
dy = height
if vertical_justification == TextProperty.VerticalJustification.VTK_TEXT_TOP:
y0 = 1.0 - (dy + y0)
dy = height
if vertical_justification == TextProperty.VerticalJustification.VTK_TEXT_CENTERED:
y0 = 0.5 - (dy / 2.0 + y0)
dy = height
name_len_min = 0
name_len_max = 0
first = True
for k in names:
sz = len(k)
if first:
name_len_min = name_len_max = sz
first = False
else:
name_len_min = min(name_len_min, sz)
name_len_max = max(name_len_max, sz)
text_positions = dict()
for k in names:
sz = len(k)
delta_sz = width * sz / name_len_max
if delta_sz > width:
delta_sz = width
if justification == TextProperty.Justification.VTK_TEXT_CENTERED:
x0 = 0.5 - delta_sz / 2.0
elif justification == TextProperty.Justification.VTK_TEXT_RIGHT:
x0 = 1.0 - dx - delta_sz
else:
# Default is left justification.
x0 = dx
# For debugging!
# print(
# f'{k:16s}: (x0, y0) = ({x0:3.2f}, {y0:3.2f}), (x1, y1) = ({x0 + delta_sz:3.2f}, {y0 + dy:3.2f})'
# f', width={delta_sz:3.2f}, height={dy:3.2f}')
text_positions[k] = {'p': [x0, y0, 0], 'p2': [delta_sz, dy, 0]}
return text_positions
def print_bands_frequencies(bands, freq, precision=2):
"""
Print each band and the number of scalars in each band.
:param bands: The bands.
:param freq: The frequencies.
:param precision: The precision for the ranges in each band.
"""
prec = abs(precision)
if prec > 14:
prec = 14
if len(bands) != len(freq):
print('Bands and Frequencies must be the same size.')
return
s = f'Bands & Frequencies:\n'
total = 0
width = prec + 6
for k, v in bands.items():
total += freq[k]
for j, q in enumerate(v):
if j == 0:
s += f'{k:4d} ['
if j == len(v) - 1:
s += f'{q:{width}.{prec}f}]: {freq[k]:8d}\n'
else:
s += f'{q:{width}.{prec}f}, '
width = 3 * width + 13
s += f'{"Total":{width}s}{total:8d}\n'
print(s)
def adjust_camera_parameters(surface_name, ren):
"""
Adjust the camera parameters.
:param surface_name: The name of the surface.
:param ren: The surface renderer.
"""
if surface_name == 'hills':
camera = ren.active_camera
camera.position = (16.3424, 19.8311, 0.46492)
camera.focal_point = (0.209609, 0.432443, -1.18699)
camera.view_up = (-0.755535, 0.640179, -0.13906)
camera.distance = 25.2845
camera.clipping_range = (13.1133, 37.6179)
elif surface_name == 'parametric hills':
camera = ren.GetActiveCamera()
camera.position = (10.9299, 59.1505, 24.9823)
camera.focal_point = (2.21692, 7.97545, 7.75135)
camera.view_up = (-0.230136, 0.345504, -0.909761)
camera.distance = 54.6966
camera.clipping_range = (36.3006, 77.9852)
elif surface_name == 'parametric torus':
camera = ren.active_camera
camera.position = (-1.38419, 24.2883, 34.9246)
camera.focal_point = (-2.07248e-07, 3.63658e-06, 0.016056)
camera.view_up = (0.010284, 0.821007, -0.570825)
camera.distance = 42.5493
camera.clipping_range = (25.2917, 64.5115)
elif surface_name == 'plane':
camera = ren.active_camera
camera.position = (-0.516003, 22.5763, 51.9171)
camera.focal_point = (-5.77108e-08, 0.500002, 5.80651e-06)
camera.view_up = (-0.000956134, 0.920254, -0.391321)
camera.distance = 56.4182
camera.clipping_range = (36.7854, 81.268)
elif surface_name == 'torus':
camera = ren.active_camera
camera.position = (-2.02659, 35.5605, 51.1256)
camera.focal_point = (0, 0, 0.0160508)
camera.view_up = (0.010284, 0.821007, -0.570825)
camera.distance = 62.2964
camera.clipping_range = (38.14, 92.8545)
@dataclass(frozen=True)
class BandedPolyDataContourFilter:
@dataclass(frozen=True)
class ScalarMode:
VTK_SCALAR_MODE_INDEX: int = 0
VTK_SCALAR_MODE_VALUE: int = 1
@dataclass(frozen=True)
class ColorTransferFunction:
@dataclass(frozen=True)
class ColorSpace:
VTK_CTF_RGB: int = 0
VTK_CTF_HSV: int = 1
VTK_CTF_LAB: int = 2
VTK_CTF_DIVERGING: int = 3
VTK_CTF_LAB_CIEDE2000: int = 4
VTK_CTF_STEP: int = 5
@dataclass(frozen=True)
class Scale:
VTK_CTF_LINEAR: int = 0
VTK_CTF_LOG10: int = 1
@dataclass(frozen=True)
class Curvatures:
@dataclass(frozen=True)
class CurvatureType:
VTK_CURVATURE_GAUSS: int = 0
VTK_CURVATURE_MEAN: int = 1
VTK_CURVATURE_MAXIMUM: int = 2
VTK_CURVATURE_MINIMUM: int = 3
@dataclass(frozen=True)
class Glyph3D:
@dataclass(frozen=True)
class ColorMode:
VTK_COLOR_BY_SCALE: int = 0
VTK_COLOR_BY_SCALAR: int = 1
VTK_COLOR_BY_VECTOR: int = 2
@dataclass(frozen=True)
class IndexMode:
VTK_INDEXING_OFF: int = 0
VTK_INDEXING_BY_SCALAR: int = 1
VTK_INDEXING_BY_VECTOR: int = 2
@dataclass(frozen=True)
class ScaleMode:
VTK_SCALE_BY_SCALAR: int = 0
VTK_SCALE_BY_VECTOR: int = 1
VTK_SCALE_BY_VECTORCOMPONENTS: int = 2
VTK_DATA_SCALING_OFF: int = 3
@dataclass(frozen=True)
class VectorMode:
VTK_USE_VECTOR: int = 0
VTK_USE_NORMAL: int = 1
VTK_VECTOR_ROTATION_OFF: int = 2
VTK_FOLLOW_CAMERA_DIRECTION: int = 3
@dataclass(frozen=True)
class Mapper:
@dataclass(frozen=True)
class ColorMode:
VTK_COLOR_MODE_DEFAULT: int = 0
VTK_COLOR_MODE_MAP_SCALARS: int = 1
VTK_COLOR_MODE_DIRECT_SCALARS: int = 2
@dataclass(frozen=True)
class ResolveCoincidentTopology:
VTK_RESOLVE_OFF: int = 0
VTK_RESOLVE_POLYGON_OFFSET: int = 1
VTK_RESOLVE_SHIFT_ZBUFFER: int = 2
@dataclass(frozen=True)
class ScalarMode:
VTK_SCALAR_MODE_DEFAULT: int = 0
VTK_SCALAR_MODE_USE_POINT_DATA: int = 1
VTK_SCALAR_MODE_USE_CELL_DATA: int = 2
VTK_SCALAR_MODE_USE_POINT_FIELD_DATA: int = 3
VTK_SCALAR_MODE_USE_CELL_FIELD_DATA: int = 4
VTK_SCALAR_MODE_USE_FIELD_DATA: int = 5
@dataclass(frozen=True)
class TextProperty:
@dataclass(frozen=True)
class Justification:
VTK_TEXT_LEFT: int = 0
VTK_TEXT_CENTERED: int = 1
VTK_TEXT_RIGHT: int = 2
@dataclass(frozen=True)
class VerticalJustification:
VTK_TEXT_BOTTOM: int = 0
VTK_TEXT_CENTERED: int = 1
VTK_TEXT_TOP: int = 2
if __name__ == '__main__':
import sys
main(sys.argv)