diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index aed91788..a6f89584 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -91,6 +91,7 @@ jobs: with: python-version: ${{ env.MAIN_PYTHON_VERSION }} add-pdf-html-docs-as-assets: true + optional-dependencies-name: 'doc,all' smoke-tests: name: Build and Smoke tests @@ -132,6 +133,7 @@ jobs: with: python-version: ${{ env.MAIN_PYTHON_VERSION }} requires-xvfb: true + optional-dependencies-name: 'tests,all' - name: Upload PyVista generated images (cache and results) if: always() diff --git a/doc/changelog.d/378.maintenance.md b/doc/changelog.d/378.maintenance.md new file mode 100644 index 00000000..caa1dfba --- /dev/null +++ b/doc/changelog.d/378.maintenance.md @@ -0,0 +1 @@ +Feat: Add Plotly as backend diff --git a/examples/01-basic-plotly-examples/README.txt b/examples/01-basic-plotly-examples/README.txt new file mode 100644 index 00000000..42cc99fc --- /dev/null +++ b/examples/01-basic-plotly-examples/README.txt @@ -0,0 +1,4 @@ +Basic Plotly usage examples +=========================== + +These examples show how to use the general plotter with Plotly backend included in the Visualization Interface Tool. \ No newline at end of file diff --git a/examples/01-basic-plotly-examples/plain-usage.py b/examples/01-basic-plotly-examples/plain-usage.py new file mode 100644 index 00000000..94c21950 --- /dev/null +++ b/examples/01-basic-plotly-examples/plain-usage.py @@ -0,0 +1,130 @@ +# Copyright (C) 2024 - 2025 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_plain_usage_plotly: + +================================= +Plain usage of the plotly backend +================================= + +This example shows the plain usage of the Plotly backend in the Visualization Interface Tool to plot different objects, +including PyVista meshes, custom objects, and Plotly-specific objects. +""" + +from ansys.tools.visualization_interface.backends.plotly.plotly_interface import PlotlyBackend +from ansys.tools.visualization_interface.types.mesh_object_plot import MeshObjectPlot +from ansys.tools.visualization_interface import Plotter +import pyvista as pv +from plotly.graph_objects import Mesh3d + + +# Create a plotter with the Plotly backend +pl = Plotter(backend=PlotlyBackend()) + +# Create a PyVista mesh +mesh = pv.Sphere() + +# Plot the mesh +pl.plot(mesh) + + +# Create a PyVista MultiBlock +multi_block = pv.MultiBlock() +multi_block.append(pv.Sphere(center=(-1, -1, 0))) +multi_block.append(pv.Cube(center=(-1, 1, 0))) + +# Plot the MultiBlock +pl.plot(multi_block) + +##################### +# Display the plotter +# +# code-block:: python +# +# pl.show() + +# Now create a custom object +class CustomObject: + def __init__(self): + self.name = "CustomObject" + self.mesh = pv.Cube(center=(1, 1, 0)) + + def get_mesh(self): + return self.mesh + + def name(self): + return self.name + + +# Create a custom object +custom_cube = CustomObject() +custom_cube.name = "CustomCube" + +# Create a MeshObjectPlot instance +mesh_object_cube = MeshObjectPlot(custom_cube, custom_cube.get_mesh()) + +# Plot the custom mesh object +pl.plot(mesh_object_cube) + +########################### +# Display the plotter again +# ========================= +# Since Plotly is a web-based visualization, we can show the plot again to include the new object. +# +# code-block:: python +# +# pl.show() + +# Add a Plotly Mesh3d object directly +custom_mesh3d = Mesh3d( + x=[0, 1, 2], + y=[0, 1, 0], + z=[0, 0, 1], + i=[0], + j=[1], + k=[2], + color='lightblue', + opacity=0.50 +) +pl.plot(custom_mesh3d) + +# Show other plotly objects like Scatter3d +from plotly.graph_objects import Scatter3d + +scatter = Scatter3d( + x=[0, 1, 2], + y=[0, 1, 0], + z=[0, 0, 1], + mode='markers', + marker=dict(size=5, color='red') +) +pl.plot(scatter) + + + +########################### +# Display the plotter again +# ========================= +# +# code-block:: python +# +# pl.show() \ No newline at end of file diff --git a/examples/01-advanced-pyansys-examples/README.txt b/examples/99-advanced-pyansys-examples/README.txt similarity index 100% rename from examples/01-advanced-pyansys-examples/README.txt rename to examples/99-advanced-pyansys-examples/README.txt diff --git a/examples/01-advanced-pyansys-examples/mixing_elbow.vtk b/examples/99-advanced-pyansys-examples/mixing_elbow.vtk similarity index 100% rename from examples/01-advanced-pyansys-examples/mixing_elbow.vtk rename to examples/99-advanced-pyansys-examples/mixing_elbow.vtk diff --git a/examples/01-advanced-pyansys-examples/using_meshobject_with_field_data.py b/examples/99-advanced-pyansys-examples/using_meshobject_with_field_data.py similarity index 100% rename from examples/01-advanced-pyansys-examples/using_meshobject_with_field_data.py rename to examples/99-advanced-pyansys-examples/using_meshobject_with_field_data.py diff --git a/pyproject.toml b/pyproject.toml index 4ed1cba1..c81afaa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,19 @@ pyvistaqt = [ "pyside6 >= 6.8.0,<7", "pyvistaqt >= 0.11.1,<1", ] + +plotly = [ + "plotly >= 6.3.1,<7", + "kaleido >= 1.1.0,<2", +] + +all = [ + "pyside6 >= 6.8.0,<7", + "pyvistaqt >= 0.11.1,<1", + "plotly >= 6.3.1,<7", + "kaleido >= 1.1.0,<2", +] + tests = [ "pytest==8.4.2", "pyvista==0.46.3", diff --git a/src/ansys/tools/visualization_interface/backends/plotly/__init__.py b/src/ansys/tools/visualization_interface/backends/plotly/__init__.py new file mode 100644 index 00000000..fedfda6f --- /dev/null +++ b/src/ansys/tools/visualization_interface/backends/plotly/__init__.py @@ -0,0 +1,22 @@ +# Copyright (C) 2024 - 2025 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. +"""Plotly initialization.""" diff --git a/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py b/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py new file mode 100644 index 00000000..33895f7a --- /dev/null +++ b/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py @@ -0,0 +1,206 @@ +# Copyright (C) 2024 - 2025 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. + +"""Plotly backend interface for visualization.""" +from typing import Any, Iterable, Union + +import plotly.graph_objects as go +import pyvista as pv +from pyvista import PolyData + +from ansys.tools.visualization_interface.backends._base import BaseBackend +from ansys.tools.visualization_interface.types.mesh_object_plot import MeshObjectPlot + + +class PlotlyBackend(BaseBackend): + """Plotly interface for visualization.""" + + def __init__(self) -> None: + """Initialize the Plotly backend.""" + self._fig = go.Figure() + + def _pv_to_mesh3d(self, pv_mesh: Union[PolyData, pv.MultiBlock]) -> Union[go.Mesh3d, list]: + """Convert a PyVista PolyData or MultiBlock mesh to Plotly Mesh3d format. + + Parameters + ---------- + pv_mesh : Union[PolyData, pv.MultiBlock] + The PyVista PolyData or MultiBlock mesh to convert. + + Returns + ------- + Union[go.Mesh3d, list] + The converted Plotly Mesh3d object(s). Returns a single Mesh3d for PolyData, + or a list of Mesh3d objects for MultiBlock. + """ + if isinstance(pv_mesh, pv.MultiBlock): + # Handle MultiBlock by converting each block and returning a list + mesh_list = [] + for i, block in enumerate(pv_mesh): + if block is not None: + # Convert each block to PolyData if needed + if hasattr(block, 'extract_surface'): + # For volume meshes, extract the surface + block = block.extract_surface() + elif not isinstance(block, PolyData): + # Try to convert to PolyData + try: + block = block.cast_to_polydata() + except AttributeError: + continue # Skip blocks that can't be converted + + # Now convert the PolyData block + mesh_3d = self._convert_polydata_to_mesh3d(block) + mesh_list.append(mesh_3d) + return mesh_list + else: + # Handle single PolyData + return self._convert_polydata_to_mesh3d(pv_mesh) + + def _convert_polydata_to_mesh3d(self, pv_mesh: PolyData) -> go.Mesh3d: + """Convert a single PolyData mesh to Plotly Mesh3d format. + + Parameters + ---------- + pv_mesh : PolyData + The PyVista PolyData mesh to convert. + + Returns + ------- + go.Mesh3d + The converted Plotly Mesh3d object. + """ + points = pv_mesh.points + x, y, z = points[:, 0], points[:, 1], points[:, 2] + + # Convert mesh to triangular mesh if needed, since Plotly only supports triangular faces + triangulated_mesh = pv_mesh.triangulate() + + # Extract triangular faces + faces = triangulated_mesh.faces.reshape((-1, 4)) # Now we know all faces are triangular (3 vertices + count) + i, j, k = faces[:, 1], faces[:, 2], faces[:, 3] + + return go.Mesh3d(x=x, y=y, z=z, i=i, j=j, k=k) + + @property + def layout(self) -> Any: + """Get the current layout of the Plotly figure. + + Returns + ------- + Any + The current layout of the Plotly figure. + """ + return self._fig.layout + + @layout.setter + def layout(self, new_layout: Any) -> None: + """Set a new layout for the Plotly figure. + + Parameters + ---------- + new_layout : Any + New layout to set for the Plotly figure. + """ + self._fig.update_layout(new_layout) + + def plot_iter(self, plotting_list: Iterable[Any]) -> None: + """Plot multiple objects using Plotly. + + Parameters + ---------- + plotting_list : Iterable[Any] + An iterable of objects to plot. + """ + for item in plotting_list: + self.plot(item) + + + def plot( + self, + plottable_object: Union[PolyData, pv.MultiBlock, MeshObjectPlot, go.Mesh3d], + **plotting_options + ) -> None: + """Plot a single object using Plotly. + + Parameters + ---------- + plottable_object : Union[PolyData, pv.MultiBlock, MeshObjectPlot, go.Mesh3d] + The object to plot. Can be a PyVista PolyData, MultiBlock, a MeshObjectPlot, or a Plotly Mesh3d. + plotting_options : dict + Additional plotting options. + """ + if isinstance(plottable_object, MeshObjectPlot): + mesh = plottable_object.mesh + else: + mesh = plottable_object + + if isinstance(mesh, (PolyData, pv.MultiBlock)): + mesh_result = self._pv_to_mesh3d(mesh) + + # Handle both single mesh and list of meshes + if isinstance(mesh_result, list): + # MultiBlock case - add all meshes + for mesh_3d in mesh_result: + self._fig.add_trace(mesh_3d) + else: + # Single PolyData case + self._fig.add_trace(mesh_result) + elif isinstance(plottable_object, go.Mesh3d): + self._fig.add_trace(plottable_object) + else: + try: + self._fig.add_trace(plottable_object) + except Exception: + raise TypeError("Unsupported plottable_object type for PlotlyInterface.") + + def show(self, + plottable_object=None, + screenshot: str = None, + name_filter=None, + **kwargs) -> None: + """Render the Plotly scene. + + Parameters + ---------- + plottable_object : Any, optional + Object to show, by default None. + screenshot : str, optional + Path to save a screenshot, by default None. + name_filter : bool, optional + Flag to filter the object, by default None. + kwargs : dict + Additional options the selected backend accepts. + """ + if plottable_object is not None: + self.plot(plottable_object) + + + # Only show in browser if no screenshot is being taken + if not screenshot: + self._fig.show(**kwargs) + else: + screenshot_str = str(screenshot) + if screenshot_str.endswith('.html'): + self._fig.write_html(screenshot_str) + else: + self._fig.write_image(screenshot_str) diff --git a/src/ansys/tools/visualization_interface/plotter.py b/src/ansys/tools/visualization_interface/plotter.py index 659e7da3..136d7465 100644 --- a/src/ansys/tools/visualization_interface/plotter.py +++ b/src/ansys/tools/visualization_interface/plotter.py @@ -49,6 +49,18 @@ def backend(self): """Return the base plotter object.""" return self._backend + def plot_iter(self, plotting_list: List, **plotting_options): + """Plots multiple objects using the specified backend. + + Parameters + ---------- + plotting_list : List + List of objects to plot. + plotting_options : dict + Additional plotting options. + """ + self._backend.plot_iter(plotting_list=plotting_list, **plotting_options) + def plot(self, plottable_object: Any, **plotting_options): """Plots an object using the specified backend. diff --git a/src/ansys/tools/visualization_interface/types/edge_plot.py b/src/ansys/tools/visualization_interface/types/edge_plot.py index 04574402..03d6068a 100644 --- a/src/ansys/tools/visualization_interface/types/edge_plot.py +++ b/src/ansys/tools/visualization_interface/types/edge_plot.py @@ -22,11 +22,13 @@ """Provides the edge type for plotting.""" -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Union import pyvista as pv if TYPE_CHECKING: + from plotly.graph_objects import Mesh3d + from ansys.tools.visualization_interface.types.mesh_object_plot import MeshObjectPlot class EdgePlot: @@ -34,7 +36,7 @@ class EdgePlot: Parameters ---------- - actor : ~pyvista.Actor + actor : Union[~pyvista.Actor, Mesh3d] PyVista actor that represents the edge. edge_object : Edge PyAnsys object edge that is represented by the PyVista actor. @@ -43,7 +45,7 @@ class EdgePlot: """ - def __init__(self, actor: pv.Actor, edge_object: Any, parent: Any = None) -> None: + def __init__(self, actor: Union[pv.Actor, "Mesh3d"], edge_object: Any, parent: Any = None) -> None: """Initialize ``EdgePlot`` variables.""" self._actor = actor self._object = edge_object diff --git a/src/ansys/tools/visualization_interface/types/mesh_object_plot.py b/src/ansys/tools/visualization_interface/types/mesh_object_plot.py index 853630d9..5a106199 100644 --- a/src/ansys/tools/visualization_interface/types/mesh_object_plot.py +++ b/src/ansys/tools/visualization_interface/types/mesh_object_plot.py @@ -22,12 +22,14 @@ """Provides the ``MeshObjectPlot`` class.""" -from typing import Any, List, Union +from typing import TYPE_CHECKING, Any, List, Type, Union import pyvista as pv from ansys.tools.visualization_interface.types.edge_plot import EdgePlot +if TYPE_CHECKING: + from plotly.graph_objects import Mesh3d class MeshObjectPlot: """Relates a custom object with a mesh, provided by the consumer library.""" @@ -35,7 +37,7 @@ class MeshObjectPlot: def __init__( self, custom_object: Any, - mesh: Union[pv.PolyData, pv.MultiBlock], + mesh: Union[pv.PolyData, pv.MultiBlock, "Mesh3d"], actor: pv.Actor = None, edges: List[EdgePlot] = None, ) -> None: @@ -49,7 +51,7 @@ def __init__( ---------- custom_object : Any Any object that the consumer library wants to relate with a mesh. - mesh : Union[pv.PolyData, pv.MultiBlock] + mesh : Union[pv.PolyData, pv.MultiBlock, Mesh3d] PyVista mesh that represents the custom object. actor : pv.Actor, default: None Actor of the mesh in the plotter. @@ -63,7 +65,7 @@ def __init__( self._edges = edges @property - def mesh(self) -> Union[pv.PolyData, pv.MultiBlock]: + def mesh(self) -> Union[pv.PolyData, pv.MultiBlock, "Mesh3d"]: """Mesh of the object in PyVista format. Returns @@ -75,12 +77,12 @@ def mesh(self) -> Union[pv.PolyData, pv.MultiBlock]: return self._mesh @mesh.setter - def mesh(self, mesh: Union[pv.PolyData, pv.MultiBlock]): + def mesh(self, mesh: Union[pv.PolyData, pv.MultiBlock, "Mesh3d"]): """Set the mesh of the object in PyVista format. Parameters ---------- - mesh : Union[pv.PolyData, pv.MultiBlock] + mesh : Union[pv.PolyData, pv.MultiBlock, Mesh3d] Mesh of the object. """ @@ -174,3 +176,15 @@ def name(self) -> str: return self._custom_object.id else: return "Unknown" + + @property + def mesh_type(self) -> Type: + """Type of the mesh. + + Returns + ------- + type + Type of the mesh. + + """ + return type(self._mesh) \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index e1c27ec9..98826461 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,23 +21,53 @@ # SOFTWARE. """Conftest file for unit tests.""" import os +from pathlib import Path +from PIL import Image, ImageChops import pytest os.environ.setdefault("PYANSYS_VISUALIZER_TESTMODE", "true") -@pytest.fixture(autouse=True) -def wrapped_verify_image_cache(verify_image_cache): - """Wraps the verify_image_cache fixture to ensure that the image cache is verified. - - Parameters - ---------- - verify_image_cache : fixture - Fixture to wrap. - - Returns - ------- - fixture - Wrapped fixture. - """ - return verify_image_cache +@pytest.fixture +def image_compare(): + """Fixture to compare images.""" + def _compare_images(generated_image_path): + """Compare two images and optionally save the difference image. + + Parameters + ---------- + generated_image_path : str + Path to the generated image. + baseline_image_path : str + Path to the baseline image. + diff_image_path : str, optional + Path to save the difference image if images do not match. + + Returns + ------- + bool + True if images match, False otherwise. + """ + # Get the name of the image file using Pathlib + image_name = Path(generated_image_path).name + + # Define the baseline image path + baseline_image_path = Path(__file__).parent / "_image_cache" / image_name + + img1 = Image.open(generated_image_path).convert("RGB") + try: + img2 = Image.open(baseline_image_path).convert("RGB") + except FileNotFoundError: + # copy generated image to baseline location if baseline does not exist + img1.save(baseline_image_path) + img2 = Image.open(baseline_image_path).convert("RGB") + + # Compute the difference between the two images + diff = ImageChops.difference(img1, img2) + + if diff.getbbox() is None: + return True + else: + return False + + return _compare_images \ No newline at end of file diff --git a/tests/test_generic_plotter.py b/tests/test_generic_plotter.py index 1377321c..03ea0838 100644 --- a/tests/test_generic_plotter.py +++ b/tests/test_generic_plotter.py @@ -42,6 +42,23 @@ def __init__(self, name) -> None: self.name = name +@pytest.fixture(autouse=True) +def wrapped_verify_image_cache(verify_image_cache): + """Wraps the verify_image_cache fixture to ensure that the image cache is verified. + + Parameters + ---------- + verify_image_cache : fixture + Fixture to wrap. + + Returns + ------- + fixture + Wrapped fixture. + """ + return verify_image_cache + + def test_plotter_add_pd(): """Adds polydata to the plotter.""" pl = Plotter() diff --git a/tests/test_plotly_backend.py b/tests/test_plotly_backend.py new file mode 100644 index 00000000..1d685480 --- /dev/null +++ b/tests/test_plotly_backend.py @@ -0,0 +1,161 @@ +# Copyright (C) 2024 - 2025 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. +"""Test module for plotly backend.""" +from plotly.graph_objects import Mesh3d, Scatter3d +import pyvista as pv + +from ansys.tools.visualization_interface import Plotter +from ansys.tools.visualization_interface.backends.plotly.plotly_interface import PlotlyBackend +from ansys.tools.visualization_interface.types.mesh_object_plot import MeshObjectPlot + + +def test_plot_3dmesh(tmp_path, image_compare): + """Test plotting a Plotly Mesh3d object.""" + # Create a plotter with the Plotly backend + pl = Plotter(backend=PlotlyBackend()) + + # Create a Plotly Mesh3d object + mesh3d = Mesh3d( + x=[0, 1, 2], + y=[0, 1, 0], + z=[0, 0, 1], + i=[0], + j=[1], + k=[2], + color='lightblue', + opacity=0.50 + ) + + # Plot the Mesh3d object + pl.plot(mesh3d) + + # Show the plot (this will open a browser window) + file = tmp_path / "test_plot_3dmesh.png" + pl.show(screenshot=file) + assert image_compare(file) + + +def test_plot_pyvista_mesh(tmp_path, image_compare): + """Test plotting a PyVista mesh.""" + # Create a plotter with the Plotly backend + pl = Plotter(backend=PlotlyBackend()) + + # Create a PyVista mesh (e.g., a sphere) + mesh = pv.Sphere() + + # Plot the PyVista mesh + pl.plot(mesh) + + # Show the plot (this will open a browser window) + file = tmp_path / "test_plot_pyvista_mesh.png" + pl.show(screenshot=file) + assert image_compare(file) + + +def test_plot_pyvista_multiblock(tmp_path, image_compare): + """Test plotting a PyVista MultiBlock mesh.""" + # Create a plotter with the Plotly backend + pl = Plotter(backend=PlotlyBackend()) + + # Create a PyVista MultiBlock + multi_block = pv.MultiBlock() + multi_block.append(pv.Sphere(center=(-1, -1, 0))) + multi_block.append(pv.Cube(center=(-1, 1, 0))) + + # Plot the MultiBlock + pl.plot(multi_block) + + # Show the plot (this will open a browser window) + file = tmp_path / "test_plot_pyvista_multiblock.png" + pl.show(screenshot=file) + assert image_compare(file) + + +def test_plot_mesh_object_plot(tmp_path, image_compare): + """Test plotting a MeshObjectPlot.""" + # Create a plotter with the Plotly backend + pl = Plotter(backend=PlotlyBackend()) + + # Create a custom object with a get_mesh method + class CustomObject: + def __init__(self): + self.name = "CustomObject" + self.mesh = pv.Cube(center=(1, 1, 0)) + + def get_mesh(self): + return self.mesh + + def name(self): + return self.name + + custom_obj = CustomObject() + + # Create a MeshObjectPlot instance + + mesh_object = MeshObjectPlot(custom_obj, custom_obj.get_mesh()) + + # Plot the MeshObjectPlot + pl.plot(mesh_object) + + # Show the plot (this will open a browser window) + file = tmp_path / "test_plot_mesh_object_plot.png" + pl.show(screenshot=file) + assert image_compare(file) + + +def test_plot_scatter_points(tmp_path, image_compare): + """Test plotting scatter points using Plotly.""" + # Create a plotter with the Plotly backend + pl = Plotter(backend=PlotlyBackend()) + + # Create some scatter points + scatter_points = Scatter3d( + x=[0, 1, 2, 3], + y=[0, 1, 0, 1], + z=[0, 0, 1, 1], + mode='markers', + marker=dict(size=5, color='red') + ) + + # Plot the scatter points + pl.plot(scatter_points) + + # Show the plot (this will open a browser window) + file = tmp_path / "test_plot_scatter_points.png" + pl.show(screenshot=file) + assert image_compare(file) + +def test_plot_iter(tmp_path, image_compare): + """Test plotting multiple objects using Plotly.""" + # Create a plotter with the Plotly backend + pl = Plotter(backend=PlotlyBackend()) + + # Create a list of PyVista meshes + meshes = [pv.Sphere(), pv.Cube(), pv.Cylinder()] + + # Plot the meshes + pl.plot_iter(meshes) + + # Show the plot (this will open a browser window) + file = tmp_path / "test_plot_iter.png" + pl.show(screenshot=file) + assert image_compare(file) \ No newline at end of file