diff --git a/doc/changelog.d/405.miscellaneous.md b/doc/changelog.d/405.miscellaneous.md new file mode 100644 index 00000000..394fe58c --- /dev/null +++ b/doc/changelog.d/405.miscellaneous.md @@ -0,0 +1 @@ +Feat: Add optional tree structure to MeshObjectPlot class diff --git a/examples/00-basic-pyvista-examples/tree_struct.py b/examples/00-basic-pyvista-examples/tree_struct.py new file mode 100644 index 00000000..d2e51c92 --- /dev/null +++ b/examples/00-basic-pyvista-examples/tree_struct.py @@ -0,0 +1,75 @@ +# 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: + +============================= +MeshObjectPlot tree structure +============================= + +This example shows how to add a tree structure of MeshObjectPlot to the plotter. +""" +import pyvista as pv +from ansys.tools.visualization_interface import Plotter + +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 objects +custom_cube = CustomObject() +custom_cube.name = "CustomCube" + +custom_sphere = CustomObject() +custom_sphere.mesh = pv.Sphere(center=(0, 0, 5)) +custom_sphere.name = "CustomSphere" + +custom_sphere1 = CustomObject() +custom_sphere1.mesh = pv.Sphere(center=(5, 0, 5)) +custom_sphere1.name = "CustomSphere" + +from ansys.tools.visualization_interface import MeshObjectPlot + +# Create an instance +mesh_object_cube = MeshObjectPlot(custom_cube, custom_cube.get_mesh()) +mesh_object_sphere = MeshObjectPlot(custom_sphere, custom_sphere.get_mesh()) +mesh_object_sphere1 = MeshObjectPlot(custom_sphere1, custom_sphere1.get_mesh()) + +mesh_object_cube.add_child(mesh_object_sphere) +mesh_object_sphere.add_child(mesh_object_sphere1) + +pl = Plotter() +pl.plot(mesh_object_cube, plot_children=True) + + +pl.backend._pl.hide_children(mesh_object_cube) +pl.show() \ No newline at end of file diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py index 1708cd3d..a76c90cc 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py @@ -186,13 +186,15 @@ def clip( return mesh.clip(normal=[elem for elem in plane.normal], origin=plane.origin) - def plot_meshobject(self, custom_object: MeshObjectPlot, **plotting_options): + def plot_meshobject(self, custom_object: MeshObjectPlot, plot_children: bool = True, **plotting_options): """Plot a generic ``MeshObjectPlot`` object to the scene. Parameters ---------- plottable_object : MeshObjectPlot Object to add to the scene. + plot_children : bool, default: True + Whether to plot the children of the object. **plotting_options : dict, default: None Keyword arguments. For allowable keyword arguments, see the :meth:`Plotter.add_mesh ` method. @@ -205,6 +207,11 @@ def plot_meshobject(self, custom_object: MeshObjectPlot, **plotting_options): actor = self.scene.add_mesh(dataset, **plotting_options) custom_object.actor = actor self._object_to_actors_map[actor] = custom_object + + if plot_children: + for child in custom_object._children: + self.plot_meshobject(child, plot_children=plot_children, **plotting_options) + return actor.name def plot_edges(self, custom_object: MeshObjectPlot, **plotting_options) -> None: @@ -242,10 +249,40 @@ def plot_edges(self, custom_object: MeshObjectPlot, **plotting_options) -> None: else: logger.warning("The object does not have edges.") + + def hide_children(self, custom_object: MeshObjectPlot) -> None: + """Hide all the children of a given ``MeshObjectPlot`` object. + + Parameters + ---------- + custom_object : MeshObjectPlot + Custom object whose children will be hidden. + + """ + for child in custom_object._children: + if child.actor: + child.actor.SetVisibility(False) + self.hide_children(child) + + def show_children(self, custom_object: MeshObjectPlot) -> None: + """Show all the children of a given ``MeshObjectPlot`` object. + + Parameters + ---------- + custom_object : MeshObjectPlot + Custom object whose children will be shown. + + """ + for child in custom_object._children: + if child.actor: + child.actor.SetVisibility(True) + self.show_children(child) + def plot( self, plottable_object: Union[pv.PolyData, pv.MultiBlock, MeshObjectPlot, pv.UnstructuredGrid], name_filter: str = None, + plot_children: bool = False, **plotting_options, ) -> None: """Plot any type of object to the scene. @@ -287,7 +324,7 @@ def plot( else: self.scene.add_composite(plottable_object, **plotting_options) elif isinstance(plottable_object, MeshObjectPlot): - self.plot_meshobject(plottable_object, **plotting_options) + self.plot_meshobject(plottable_object, plot_children=plot_children, **plotting_options) else: logger.warning("The object type is not supported. ") 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 5a106199..713b6858 100644 --- a/src/ansys/tools/visualization_interface/types/mesh_object_plot.py +++ b/src/ansys/tools/visualization_interface/types/mesh_object_plot.py @@ -40,6 +40,8 @@ def __init__( mesh: Union[pv.PolyData, pv.MultiBlock, "Mesh3d"], actor: pv.Actor = None, edges: List[EdgePlot] = None, + children: List["MeshObjectPlot"] = None, + parent: "MeshObjectPlot" = None, ) -> None: """Relates a custom object with a mesh provided by the consumer library. @@ -63,6 +65,51 @@ def __init__( self._mesh = mesh self._actor = actor self._edges = edges + self._children: List["MeshObjectPlot"] = children if children is not None else [] + self._parent: "MeshObjectPlot" = parent + + def add_child(self, child: "MeshObjectPlot"): + """Set a child MeshObjectPlot to the current object. + + This method is used to set a child MeshObjectPlot to the current object. + It is useful when the custom object has a hierarchical structure, and + the consumer library wants to relate the child objects with their meshes. + + Parameters + ---------- + child : MeshObjectPlot + Child MeshObjectPlot to be set. + + """ + child.parent = self + self._children.append(child) + + @property + def parent(self) -> "MeshObjectPlot": + """Get the parent MeshObjectPlot of the current object. + + This method is used to set a parent MeshObjectPlot to the current object. + It is useful when the custom object has a hierarchical structure, and + the consumer library wants to relate the parent objects with their meshes. + + Parameters + ---------- + parent : MeshObjectPlot + Parent MeshObjectPlot to be set. + + """ + return self._parent + + @parent.setter + def parent(self, parent: "MeshObjectPlot"): + """Set the parent MeshObjectPlot of the current object. + + Parameters + ---------- + parent : MeshObjectPlot + Parent MeshObjectPlot to be set. + """ + self._parent = parent @property def mesh(self) -> Union[pv.PolyData, pv.MultiBlock, "Mesh3d"]: diff --git a/tests/test_meshobject.py b/tests/test_meshobject.py new file mode 100644 index 00000000..5f0766ed --- /dev/null +++ b/tests/test_meshobject.py @@ -0,0 +1,62 @@ +# 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 the generic plotter.""" +import os + +import pyvista as pv + +from ansys.tools.visualization_interface import MeshObjectPlot + +IN_GITHUB_ACTIONS = os.getenv("IN_GITHUB_ACTIONS") == "true" + + +class CustomTestClass: + """Mock custom class for testing MeshObjectPlot.""" + + def __init__(self, name) -> None: + """Mock init.""" + self.name = name + + + +def test_mesh_object_plot_tree(): + """Test that basic parent-child relationships work.""" + parent_mesh = pv.Sphere() + child_mesh = pv.Cube() + + parent_obj = MeshObjectPlot(CustomTestClass("parent"), parent_mesh) + child_obj = MeshObjectPlot(CustomTestClass("child"), child_mesh) + + parent_obj.add_child(child_obj) + + + # assert that the child's parent is set correctly + assert child_obj.parent == parent_obj + + # assert that the parent's children contain the child + assert child_obj in parent_obj._children + + # assert that the parent's parent is None + assert parent_obj.parent is None + + # assert that the child has no children + assert len(child_obj._children) == 0