Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions examples/00-basic-pyvista-examples/remote_trame_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""
.. _ref_remote_trame_view:

=============================
Use trame as a remote service
=============================

This example shows how to launch a trame service and use it as a remote service.

First, we need to launch the trame service. We can do this by running the following code::

# import required libraries
from ansys.tools.visualization_interface.backends.pyvista.trame_service import (
TrameService,
)

# create a trame service, in whatever port is available in your system
ts = TrameService(websocket_port=8765)

# run the service
ts.run()


Now, we can send meshes and plotter to the trame service. We can do this by running the following code in a separate terminal::

# import required libraries
import time

import pyvista as pv

from ansys.tools.visualization_interface.backends.pyvista.trame_remote import (
send_mesh,
send_pl,
)

# create an example plotter
plotter = pv.Plotter()
plotter.add_mesh(pv.Cube())

# send some example meshes
send_mesh(pv.Sphere())
send_mesh(pv.Sphere(center=(3, 0, 0)))
time.sleep(4)

# if we send a plotter, the previous meshes will be deleted.
send_pl(plotter)

"""
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ classifiers = [
]

dependencies = [
"pyvista >= 0.42.0",
"beartype >= 0.17.0",
"pyvista >= 0.42.0,<1",
"beartype >= 0.17.0,<1",
"websockets >= 12.0,<13",
"trame >= 3.6.0,<4",
"trame-vtk >= 2.8.7,<3",
"trame-vuetify >= 2.4.3,<3",
]

[project.optional-dependencies]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from ansys.tools.visualization_interface import USE_TRAME
from ansys.tools.visualization_interface.backends._base import BaseBackend
from ansys.tools.visualization_interface.backends.pyvista.pyvista_interface import PyVistaInterface
from ansys.tools.visualization_interface.backends.pyvista.trame_gui import (
from ansys.tools.visualization_interface.backends.pyvista.trame_local import (
_HAS_TRAME,
TrameVisualizer,
)
Expand Down Expand Up @@ -280,7 +280,7 @@ def compute_edge_object_map(self) -> Dict[pv.Actor, EdgePlot]:
"""Compute the mapping between plotter actors and ``EdgePlot`` objects.

Returns
--------
-------
Dict[~pyvista.Actor, EdgePlot]
Dictionary defining the mapping between plotter actors and ``EdgePlot`` objects.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright (C) 2024 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Module for trame websocket client functions."""
import pickle

from beartype.typing import Union
import pyvista as pv
from websockets.sync.client import connect


def send_pl(plotter: pv.Plotter, host: str = "localhost", port: int = 8765):
"""Send the plotter meshes to a remote trame service.

Since plotter can't be pickled, we send the meshes list instead.

Parameters
----------
plotter : pv.Plotter
Plotter to send.
host : str, optional
Websocket host to connect to, by default "localhost".
port : int, optional
Websocket port to connect to, by default 8765.
"""
with connect("ws://" + host + ":" + str(port)) as websocket:
# Plotter can't be pickled, so we send the meshes list instead
meshes_list_pk = pickle.dumps(plotter.meshes)
websocket.send(meshes_list_pk)

def send_mesh(mesh: Union[pv.PolyData, pv.MultiBlock], host: str = "localhost", port: int = 8765):
"""Send a mesh to a remote trame service.

Parameters
----------
mesh : Union[pv.PolyData, pv.MultiBlock]
Mesh to send.
host : str, optional
Websocket host to connect to, by default "localhost".
port : int, optional
Websocket port to connect to, by default 8765.
"""
with connect("ws://" + host + ":" + str(port)) as websocket:
mesh_pk = pickle.dumps(mesh)
websocket.send(mesh_pk)
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Copyright (C) 2024 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""Trame service module."""
import asyncio
import pickle

import pyvista as pv
from pyvista.trame.ui import plotter_ui
from trame.app import get_server
from trame.ui.vuetify3 import SinglePageLayout
from trame.widgets import vuetify3
from websockets import WebSocketServerProtocol
from websockets.server import serve


class TrameService:
"""Trame service class.

Initializes a trame service where you can send meshes to plot in a trame webview plotter.

Parameters
----------
websocket_host : str, optional
Host where the webserver will listen for new plotters and meshes, by default "localhost".
websocket_port : int, optional
Port where the webserver will listen for new plotters and meshes, by default 8765.
"""
def __init__(self, websocket_host: str = "localhost", websocket_port: int=8765):
"""Initialize the trame service."""
pv.OFF_SCREEN = True

self._server = get_server()
self._state, self._ctrl = self._server.state, self._server.controller
self._websocket_port = websocket_port
self._websocket_host = websocket_host
# pyvista plotter, we treat it as a view i.e. created once but updated as we see fit.
self._pl = pv.Plotter()

def clear_plotter(self):
"""Clears the web view in the service."""
self._pl.clear_actors()
self._pl.reset_camera()


async def _listener(self, websocket: WebSocketServerProtocol):
"""Listens for new meshes and plotters.

Infinite loop where we wait for new messages from the client.
If we get a list of meshes, we assume it's a scene and clear the previous meshes.

Parameters
----------
websocket : websockets.WebSocketServerProtocol
Websocket where to listen.
"""
async for message in websocket:
obj = pickle.loads(message)

if isinstance(obj, list):
# if we get a list of meshes, assume it's a scene and clear previous meshes
self.clear_plotter()
for mesh in obj:
self._pl.add_mesh(mesh)
print(mesh)
else:
print(obj)
self._pl.add_mesh(obj)
self._pl.reset_camera()

async def _webserver(self):
"""Starts the webserver for the trame service listener."""
async with serve(self._listener, self._websocket_host, self._websocket_port):
await asyncio.Future() # run forever

def set_scene(self):
"""Sets the web view scene for the trame service."""
with SinglePageLayout(self._server) as layout:
with layout.toolbar:
with vuetify3.VBtn(icon=True, click=self.clear_plotter):
vuetify3.VIcon("mdi-trash-can")
with vuetify3.VBtn(icon=True, click=self._ctrl.reset_camera):
vuetify3.VIcon("mdi-crop-free")

# main view container
with layout.content:
with vuetify3.VContainer(
fluid=True, classes="pa-0 fill-height", style="position: relative;"
):
view = plotter_ui(self._pl)
# bind plotter methods to the controller method, this way we can
# attach UI elements on it. see buttons above
self._ctrl.view_update = view.update
self._ctrl.reset_camera = view.reset_camera


async def _launch_trame_service(self):
"""Launches the trame service."""
self.set_scene()
trame_coroutine = self._server.start(exec_mode="coroutine")
webserver_coroutine = asyncio.create_task(self._webserver())
await asyncio.gather(trame_coroutine, webserver_coroutine)

def run(self):
"""Start the trame web view and the websocket services."""
asyncio.run(self._launch_trame_service())