From ac75627908378354bed5251d10a92c34d209ec39 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Thu, 4 Apr 2024 07:48:07 -0500 Subject: [PATCH] first version of 3d visualization with pyvista (#680) Co-authored-by: Mohamed Koubaa Co-authored-by: pyansys-ci-bot Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/changelog.d/680.dependencies.md | 1 + pyproject.toml | 3 + src/ansys/mechanical/core/embedding/app.py | 29 +++++++ .../mechanical/core/embedding/viz/__init__.py | 23 ++++++ .../core/embedding/viz/pyvista_plotter.py | 75 +++++++++++++++++++ .../mechanical/core/embedding/viz/utils.py | 32 ++++++++ 6 files changed, 163 insertions(+) create mode 100644 doc/changelog.d/680.dependencies.md create mode 100644 src/ansys/mechanical/core/embedding/viz/__init__.py create mode 100644 src/ansys/mechanical/core/embedding/viz/pyvista_plotter.py create mode 100644 src/ansys/mechanical/core/embedding/viz/utils.py diff --git a/doc/changelog.d/680.dependencies.md b/doc/changelog.d/680.dependencies.md new file mode 100644 index 000000000..21771242b --- /dev/null +++ b/doc/changelog.d/680.dependencies.md @@ -0,0 +1 @@ +first version of 3d visualization with pyvista \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index eee5daebb..11f68d746 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,9 @@ doc = [ "sphinxcontrib-websupport==1.2.7", "sphinxemoji==0.3.1", ] +viz = [ + "pyvista>=0.39.1" +] [project.scripts] ansys-mechanical = "ansys.mechanical.core.run:cli" diff --git a/src/ansys/mechanical/core/embedding/app.py b/src/ansys/mechanical/core/embedding/app.py index 06bcf3763..d9f3148ae 100644 --- a/src/ansys/mechanical/core/embedding/app.py +++ b/src/ansys/mechanical/core/embedding/app.py @@ -23,6 +23,7 @@ """Main application class for embedded Mechanical.""" import atexit import os +import warnings from ansys.mechanical.core.embedding import initializer, runtime from ansys.mechanical.core.embedding.addins import AddinConfiguration @@ -30,6 +31,14 @@ from ansys.mechanical.core.embedding.poster import Poster from ansys.mechanical.core.embedding.warnings import connect_warnings, disconnect_warnings +try: + import pyvista # noqa: F401 + + HAS_PYVISTA = True +except: + + HAS_PYVISTA = False + def _get_default_addin_configuration() -> AddinConfiguration: configuration = AddinConfiguration() @@ -212,6 +221,26 @@ def execute_script(self, script: str): rets = None return self.script_engine.ExecuteCode(script, SCRIPT_SCOPE, light_mode, args, rets) + def plot(self) -> None: + """Visualize the model in 3d. + + Requires installation using the viz option. E.g. + pip install ansys-mechanical-core[viz] + """ + if not HAS_PYVISTA: + warnings.warn( + "Installation of viz option required! Use pip install ansys-mechanical-core[viz]" + ) + return + + if self.version < 242: + warnings.warn("Plotting is only supported with version 2024R2 and later!") + return + + from ansys.mechanical.core.embedding.viz.pyvista_plotter import plot_model + + plot_model(self) + @property def poster(self) -> Poster: """Returns an instance of Poster.""" diff --git a/src/ansys/mechanical/core/embedding/viz/__init__.py b/src/ansys/mechanical/core/embedding/viz/__init__.py new file mode 100644 index 000000000..a4270eb41 --- /dev/null +++ b/src/ansys/mechanical/core/embedding/viz/__init__.py @@ -0,0 +1,23 @@ +# Copyright (C) 2022 - 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. + +"""Namespace module for embedding visualization.""" diff --git a/src/ansys/mechanical/core/embedding/viz/pyvista_plotter.py b/src/ansys/mechanical/core/embedding/viz/pyvista_plotter.py new file mode 100644 index 000000000..ae0058af8 --- /dev/null +++ b/src/ansys/mechanical/core/embedding/viz/pyvista_plotter.py @@ -0,0 +1,75 @@ +# Copyright (C) 2022 - 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. + +"""PyVista plotter.""" + +import clr + +clr.AddReference("Ansys.Mechanical.DataModel") +clr.AddReference("Ansys.ACT.Interfaces") + +import Ansys # isort: skip + +import numpy as np +import pyvista as pv + +from .utils import bgr_to_rgb_tuple + + +def _transform_to_pyvista(transform): + return np.array([transform[i] for i in range(16)]).reshape(4, 4).transpose() + + +def _reshape_3cols(arr: np.array, name: str): + err = f"{name} must be of the form (x0,y0,z0,x1,y1,z1,...,xn,yn,zn).\ + Given {name} are not divisible by 3!" + assert arr.size % 3 == 0, err + numrows = int(arr.size / 3) + numcols = 3 + arr = np.reshape(arr, (numrows, numcols)) + return arr + + +def _get_nodes_and_coords(tri_tessellation): + np_coordinates = _reshape_3cols( + np.array(tri_tessellation.Coordinates, dtype=np.double), "coordinates" + ) + np_indices = _reshape_3cols(np.array(tri_tessellation.Indices, dtype=np.int32), "indices") + np_indices = np.insert(np_indices, 0, 3, axis=1) + return np_coordinates, np_indices + + +def plot_model(app): + """Plot the model.""" + plotter = pv.Plotter() + + for body in app.DataModel.GetObjectsByType( + Ansys.Mechanical.DataModel.Enums.DataModelObjectCategory.Body + ): + scenegraph_node = Ansys.ACT.Mechanical.Tools.ScenegraphHelpers.GetScenegraph(body) + np_coordinates, np_indices = _get_nodes_and_coords(scenegraph_node.Child) + pv_transform = _transform_to_pyvista(scenegraph_node.Transform) + polydata = pv.PolyData(np_coordinates, np_indices).transform(pv_transform) + color = pv.Color(bgr_to_rgb_tuple(body.Color)) + plotter.add_mesh(polydata, color=color, smooth_shading=True) + + plotter.show() diff --git a/src/ansys/mechanical/core/embedding/viz/utils.py b/src/ansys/mechanical/core/embedding/viz/utils.py new file mode 100644 index 000000000..ffaf5e2df --- /dev/null +++ b/src/ansys/mechanical/core/embedding/viz/utils.py @@ -0,0 +1,32 @@ +# Copyright (C) 2022 - 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. + +"""Common plotting utilities.""" +import typing + + +def bgr_to_rgb_tuple(bgr_int: int) -> typing.Tuple[int, int, int]: + """Convert bgr integer to rgb tuple.""" + r = bgr_int & 255 + g = (bgr_int >> 8) & 255 + b = (bgr_int >> 16) & 255 + return r, g, b