Vtk Interactive#

This example demonstrates the use of VTK and pyvista to display a scene whose lights, background and actor properties are edited entirely in the browser through jslink and jscallback.

import panel as pn
import pyvista as pv

from pyvista import examples

pn.extension('vtk', sizing_mode='stretch_width')

For this example we use the pyvista library to load a dataset and generate easily a VTK scene

m = examples.download_st_helens().warp_by_scalar()

# default camera position
cpos = [
    (567000.9232163235, 5119147.423216323, 6460.423216322832),
    (562835.0, 5114981.5, 2294.5),
    (-0.4082482904638299, -0.40824829046381844, 0.8164965809277649)
]

# pyvista plotter
pl = pv.Plotter(notebook=True)
actor = pl.add_mesh(m, smooth_shading=True, lighting=True)
pl.camera_position = cpos

# save initial camera properties
renderer = list(pl.ren_win.GetRenderers())[0]
initial_camera = renderer.GetActiveCamera()
initial_camera_pos = {
    "focalPoint": initial_camera.GetFocalPoint(),
    "position": initial_camera.GetPosition(),
    "viewUp": initial_camera.GetViewUp()
}

vtkpan = pn.ui.VTK(
    pl.ren_win, margin=0, sizing_mode='stretch_both', orientation_widget=True,
    enable_keybindings=True, min_height=600
)
vtkpan

Colorbars and actor selection#

# The browser looks actors up by their VTK object reference; the select only sends its label.
ACTORS = {"St Helen": actor.__this__}

actor_selection = pn.ui.Select(value="None", options=["None", *ACTORS], label="Actor Selection")
actor_selection

VTK Scene Parameters (Background, Lights,…)#

color = ''.join(['#'] + [f'{int(v*255):02x}' for v in pl.background_color[:3]])
bind_and_orient = pn.ui.CheckBoxGroup(
    value=['Orientation Widget', 'Key Bindings'], options=['Orientation Widget', 'Key Bindings']
)
reset_camera = pn.ui.Button(label='Reset Camera')
background_color = pn.ui.ColorPicker(value=color, label='Background Color')
scene_props = pn.ui.Column(bind_and_orient, reset_camera, background_color, sizing_mode='stretch_width')

light_box_title = pn.ui.StaticText(value='Light properties')
light_type = pn.ui.Select(value='HeadLight', options=['HeadLight', 'SceneLight', 'CameraLight'])
light_intensity = pn.ui.FloatSlider(start=0, end=1, value=1, label="Intensity")
light_props = pn.ui.Column(light_box_title, light_type, light_intensity, sizing_mode='stretch_width')

pn.ui.Column(scene_props, light_props, max_width=320)

Actor Properties#

Each actor property is declared once with its widget, the vtk.js expression that applies the widget value and the expression that reads it back when an actor is selected.

ACTOR_PROPERTIES = {
    'opacity': (
        pn.ui.FloatSlider(value=1, start=0, end=1, label='Opacity'),
        "property.setOpacity(source.value)", "property.getOpacity()",
    ),
    'lighting': (
        pn.ui.Toggle(value=True, label='Lighting'),
        "property.setLighting(source.value)", "!!property.getLighting()",
    ),
    'interpolation': (
        pn.ui.Select(value='Phong', options=['Flat', 'Phong'], label='Interpolation'),
        "property[`setInterpolationTo${source.value}`]()", "title(property.getInterpolationAsString())",
    ),
    'edges': (
        pn.ui.Toggle(value=False, label='Show Edges'),
        "property.setEdgeVisibility(source.value)", "!!property.getEdgeVisibility()",
    ),
    'edges_color': (
        pn.ui.ColorPicker(value='#ffffff', label='Edges Color'),
        "property.setEdgeColor(hex_to_rgb(source.value))", "rgb_to_hex(property.getEdgeColor())",
    ),
    'representation': (
        pn.ui.Select(value='Surface', options=['Points', 'Wireframe', 'Surface'], label='Representation'),
        "property[`setRepresentationTo${source.value}`]()", "title(property.getRepresentationAsString())",
    ),
    'frontface_culling': (
        pn.ui.Toggle(value=False, label='Frontface Culling'),
        "property.setFrontfaceCulling(source.value)", "!!property.getFrontfaceCulling()",
    ),
    'backface_culling': (
        pn.ui.Toggle(value=False, label='Backface Culling'),
        "property.setBackfaceCulling(source.value)", "!!property.getBackfaceCulling()",
    ),
    'ambient': (
        pn.ui.FloatSlider(value=0, start=-1, end=1, label='Ambient'),
        "property.setAmbient(source.value)", "property.getAmbient()",
    ),
    'diffuse': (
        pn.ui.FloatSlider(value=1, start=0, end=2, label='Diffuse'),
        "property.setDiffuse(source.value)", "property.getDiffuse()",
    ),
    'specular': (
        pn.ui.FloatSlider(value=0, start=0, end=10, label='Specular'),
        "property.setSpecular(source.value)", "property.getSpecular()",
    ),
    'specular_power': (
        pn.ui.FloatSlider(value=100, start=0, end=100, label='Specular Power'),
        "property.setSpecularPower(source.value)", "property.getSpecularPower()",
    ),
}
actor_widgets = {name: widget for name, (widget, _, _) in ACTOR_PROPERTIES.items()}
for widget in actor_widgets.values():
    widget.disabled = True

actor_props = pn.ui.Column(*actor_widgets.values())

actor_props

Linking all widgets together using jslinks. Material UI widgets keep their values on the model’s data, which is what source refers to in a callback; other widgets passed as args are reached through .data explicitly.

HELPERS = """
const hex_to_rgb = (color) => [1, 3, 5].map((i) => parseInt(color.slice(i, i + 2), 16) / 255)
const rgb_to_hex = (rgb) => "#" + rgb.map((c) => Math.round(255 * c).toString(16).padStart(2, "0")).join("")
const title = (text) => text[0] + text.slice(1).toLowerCase()
"""

light_type.jslink(vtkpan, code={'value': """
const light = target.renderer_el.getRenderer().getLights()[0]
light[`setLightTypeTo${source.value}`]()
target.renderer_el.getRenderWindow().render()
"""})

light_intensity.jslink(vtkpan, code={'value': """
const light = target.renderer_el.getRenderer().getLights()[0]
light.setIntensity(source.value)
target.renderer_el.getRenderWindow().render()
"""})

bind_and_orient.jslink(vtkpan, code={'value': """
target.orientation_widget = source.value.includes("Orientation Widget")
target.enable_keybindings = source.value.includes("Key Bindings")
"""})

reset_camera.js_on_click(
    args={'target': vtkpan, 'initial_camera': initial_camera_pos}, code="target.camera = initial_camera"
)

background_color.jslink(vtkpan, code={'value': HELPERS + """
target.renderer_el.getRenderer().setBackground(hex_to_rgb(source.value))
target.renderer_el.getRenderWindow().render()
"""})

for widget, setter, _ in ACTOR_PROPERTIES.values():
    widget.jscallback(args={"target": vtkpan, "actor_selection": actor_selection, "actors": ACTORS}, value=HELPERS + f"""
if (actor_selection.data.value != "None") {{
    const property = target.getActors(actors[actor_selection.data.value])[0].getProperty()
    {setter}
    target.renderer_el.getRenderWindow().render()
}}
""")

sync_widgets = "\n".join(
    f"{name}.data.setv({{value: {getter}, disabled: false}})"
    for name, (_, _, getter) in ACTOR_PROPERTIES.items()
)
disable_widgets = "\n".join(f"{name}.data.disabled = true" for name in ACTOR_PROPERTIES)

actor_selection.jslink(target=vtkpan, code={'value': HELPERS + f"""
if (source.value != "None") {{
    const actor = target.getActors(actors[source.value])[0]
    target.outline.setInputData(actor.getMapper().getInputData())
    target.renderer_el.getRenderer().addActor(target.outline_actor)
    const property = actor.getProperty()
    {sync_widgets}
}} else {{
    target.renderer_el.getRenderer().removeActor(target.outline_actor)
    {disable_widgets}
}}
target.renderer_el.getRenderWindow().render()
"""}, args={**actor_widgets, "actors": ACTORS})

controllers = pn.ui.Column(
    actor_selection,
    pn.ui.Tabs(
        ('Scene Controls', pn.ui.Column(scene_props, light_props)),
        ('Actor Props', actor_props),
        sx={'.MuiBox-root': {"overflowX": "clip"}}
    )
)

page = pn.ui.Page(
    main=[vtkpan],
    sidebar=[controllers],
    title="VTK - Mt. St Helens",
)

page

Served App#

if pn.state.served:
    page.servable()