Skip to content

Call to VTK.wasm from Python

This example capture the usage of a vtkPiker and illustate the usage of method call from Python.

Wheel picking

The code below highlight some important region and logic to understand.

py
renderer = vtk.vtkRenderer()
renderWindow = vtk.vtkRenderWindow()
renderWindow.AddRenderer(renderer)
renderWindow.OffScreenRenderingOn()

renderWindowInteractor = vtk.vtkRenderWindowInteractor()
renderWindowInteractor.SetRenderWindow(renderWindow)
renderWindowInteractor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()

for file in DATA_PATH.glob("*.vtp"):
    reader = vtk.vtkXMLPolyDataReader(file_name=str(file.resolve()))
    mapper = vtk.vtkPolyDataMapper()
    reader >> mapper
    actor = vtk.vtkActor(mapper=mapper)
    renderer.AddActor(actor)
    apply_settings(actor.property)

renderer.ResetCamera()
renderWindow.Render()

self.picker = vtk.vtkPropPicker()

self.interactor = renderWindowInteractor
self.last_picked_actor = None
self.last_picked_property = vtk.vtkProperty()
self.renderer = renderer
self.render_window = renderWindow
py
with vtklocal.LocalView(
    self.render_window,
    throttle_rate=20,
    ctx_name="wasm_view",  # attach widget on ctx
) as view:
    self.ctrl.view_update = view.update_throttle
    self.ctrl.view_reset_camera = view.reset_camera

    # ---------------------------------------------------------
    # Picker handling
    # ---------------------------------------------------------

    # => push picker to client
    view.register_vtk_object(self.picker)
    view.register_vtk_object(
        self.last_picked_property
    )  # for pure client edit

    # => attach interactor listener
    wasm_interactor_id = view.get_wasm_id(self.interactor)
    view.listeners = (
        "wasm_listeners",
        {
            wasm_interactor_id: {
                "LeftButtonPressEvent": {  # LeftButtonPressEvent, MouseMoveEvent
                    "clicked_pos": {
                        "x": (wasm_interactor_id, "EventPosition", 0),
                        "y": (wasm_interactor_id, "EventPosition", 1),
                    },
                },
            },
        },
    )
    # => reserve state variable for widget update
    self.state.clicked_pos = None
py
@change("clicked_pos")
def on_click(self, clicked_pos, **_):
    if clicked_pos is None:
        return

    if not self._picking_prending:
        asynchronous.create_task(self._pick_actor(**clicked_pos))
py
async def _pick_actor(self, x, y):
    if self._picking_prending:
        return

    try:
        self._picking_prending = True
        # Trigger a pick on client
        picked_worked = await self.ctx.wasm_view.invoke(
            self.picker, "Pick", (x, y, 0), self.renderer
        )
        if not picked_worked:
            return

        # Restore previous state
        if self.last_picked_actor:
            self.last_picked_actor.property.DeepCopy(self.last_picked_property)
            self.last_picked_actor = None

        # trame-vtklocal>=0.13.0 auto unwrap vtk object
        actor = await self.ctx.wasm_view.invoke(self.picker, "GetActor")
        if actor is None:
            return

        actor_prop = actor.property

        # Save current state and capture picked actor
        self.last_picked_property.DeepCopy(actor_prop)
        self.last_picked_actor = actor

        # Highlight actor
        actor_prop.color = (1, 0, 1)
        actor_prop.EdgeVisibilityOn()

    finally:
        # Render
        self.ctx.wasm_view.update()
        self._picking_prending = False
py
# Should import classes from vtkmodules
# but as an example we use vtk for simplicity
import vtk

from pathlib import Path

from trame.app import TrameApp, asynchronous
from trame.decorators import change
from trame.ui.vuetify3 import VAppLayout
from trame.widgets import vuetify3, vtklocal

DATA_PATH = Path(__file__).with_name("data").resolve()

RND_SEQ = vtk.vtkMinimalStandardRandomSequence()
RND_SEQ.SetSeed(8775070)


def next_color():
    rgb = []
    for _ in range(3):
        rgb.append(RND_SEQ.GetRangeValue(0.4, 1.0))
        RND_SEQ.Next()
    return rgb


def apply_settings(property):
    property.diffuse_color = next_color()
    property.diffuse = 0.8
    property.specular = 0.5
    property.specular_color = (1, 1, 1)
    property.specular_power = 30


class Pick(TrameApp):
    def __init__(self, server=None):
        super().__init__(server)
        self._picking_prending = False
        self._setup_vtk()
        self._build_ui()

    def _setup_vtk(self):
        # region vtk
        renderer = vtk.vtkRenderer()
        renderWindow = vtk.vtkRenderWindow()
        renderWindow.AddRenderer(renderer)
        renderWindow.OffScreenRenderingOn()

        renderWindowInteractor = vtk.vtkRenderWindowInteractor()
        renderWindowInteractor.SetRenderWindow(renderWindow)
        renderWindowInteractor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()

        for file in DATA_PATH.glob("*.vtp"):
            reader = vtk.vtkXMLPolyDataReader(file_name=str(file.resolve()))
            mapper = vtk.vtkPolyDataMapper()
            reader >> mapper
            actor = vtk.vtkActor(mapper=mapper)
            renderer.AddActor(actor)
            apply_settings(actor.property)

        renderer.ResetCamera()
        renderWindow.Render()

        self.picker = vtk.vtkPropPicker()

        self.interactor = renderWindowInteractor
        self.last_picked_actor = None
        self.last_picked_property = vtk.vtkProperty()
        self.renderer = renderer
        self.render_window = renderWindow
        # endregion vtk

    def _build_ui(self):
        with VAppLayout(self.server) as layout:
            self.ui = layout

            vuetify3.VBtn(
                icon="mdi-crop-free",
                click=self.ctrl.view_reset_camera,
                style="top:1rem;right:1rem;z-index:1;position:absolute;",
            )

            # region trameWidget
            with vtklocal.LocalView(
                self.render_window,
                throttle_rate=20,
                ctx_name="wasm_view",  # attach widget on ctx
            ) as view:
                self.ctrl.view_update = view.update_throttle
                self.ctrl.view_reset_camera = view.reset_camera

                # ---------------------------------------------------------
                # Picker handling
                # ---------------------------------------------------------

                # => push picker to client
                view.register_vtk_object(self.picker)
                view.register_vtk_object(
                    self.last_picked_property
                )  # for pure client edit

                # => attach interactor listener
                wasm_interactor_id = view.get_wasm_id(self.interactor)
                view.listeners = (
                    "wasm_listeners",
                    {
                        wasm_interactor_id: {
                            "LeftButtonPressEvent": {  # LeftButtonPressEvent, MouseMoveEvent
                                "clicked_pos": {
                                    "x": (wasm_interactor_id, "EventPosition", 0),
                                    "y": (wasm_interactor_id, "EventPosition", 1),
                                },
                            },
                        },
                    },
                )
                # => reserve state variable for widget update
                self.state.clicked_pos = None
            # endregion trameWidget

    # region trameChange
    @change("clicked_pos")
    def on_click(self, clicked_pos, **_):
        if clicked_pos is None:
            return

        if not self._picking_prending:
            asynchronous.create_task(self._pick_actor(**clicked_pos))

    # endregion trameChange

    # region py2wasmCall
    async def _pick_actor(self, x, y):
        if self._picking_prending:
            return

        try:
            self._picking_prending = True
            # Trigger a pick on client
            picked_worked = await self.ctx.wasm_view.invoke(
                self.picker, "Pick", (x, y, 0), self.renderer
            )
            if not picked_worked:
                return

            # Restore previous state
            if self.last_picked_actor:
                self.last_picked_actor.property.DeepCopy(self.last_picked_property)
                self.last_picked_actor = None

            # trame-vtklocal>=0.13.0 auto unwrap vtk object
            actor = await self.ctx.wasm_view.invoke(self.picker, "GetActor")
            if actor is None:
                return

            actor_prop = actor.property

            # Save current state and capture picked actor
            self.last_picked_property.DeepCopy(actor_prop)
            self.last_picked_actor = actor

            # Highlight actor
            actor_prop.color = (1, 0, 1)
            actor_prop.EdgeVisibilityOn()

        finally:
            # Render
            self.ctx.wasm_view.update()
            self._picking_prending = False

        # endregion py2wasmCall


def main():
    # region export
    import sys

    app = Pick()
    if "--export" in sys.argv:
        app.ctx.wasm_view.save("porsche.wazex")
        # Print ids
        print(f"render_window:", app.ctx.wasm_view.get_wasm_id(app.render_window))
        print(f"renderer:", app.ctx.wasm_view.get_wasm_id(app.renderer))
        print(f"property:", app.ctx.wasm_view.get_wasm_id(app.last_picked_property))
        print(f"picker:", app.ctx.wasm_view.get_wasm_id(app.picker))
        print(f"interactor:", app.ctx.wasm_view.get_wasm_id(app.interactor))

    app.server.start()
    # endregion export


if __name__ == "__main__":
    main()
txt
--extra-index-url https://wheels.vtk.org

trame>=3.9
trame-vuetify
trame-vtklocal>=0.13

# Either vtk version works
vtk==9.5.20250607.dev0
# vtk==9.5.0rc3
Window pickingFull car picking

Hight level explanation

  1. Create a vtkPicker and register it so it can be created on the WASM side.
  2. Attach a listener on the interactor so you can capture on the server side the picking location (x,y).
  3. Call "Pick" on the picker on the WASM side.
  4. If something was found, lookup the actor by making another call and convert the result into an actual vtkObject on the server side.
  5. Apply some change on the scene and push the update view to the client.

The full working code is also available.