diff --git a/src/ansys/geometry/core/math/plane.py b/src/ansys/geometry/core/math/plane.py index 7daf34e483..544678348c 100644 --- a/src/ansys/geometry/core/math/plane.py +++ b/src/ansys/geometry/core/math/plane.py @@ -81,6 +81,18 @@ def is_point_contained(self, point: Point3D) -> bool: # If plane equation is equal to 0, your point is contained return True if np.isclose(plane_eq, 0.0) else False + @property + def normal(self) -> UnitVector3D: + """ + Calculate the normal vector of the plane. + + Returns + ------- + UnitVector3D + Normal vector of the plane. + """ + return self.direction_z + @check_input_types def __eq__(self, other: "Plane") -> bool: """Equals operator for the ``Plane`` class.""" diff --git a/src/ansys/geometry/core/plotting/plotter.py b/src/ansys/geometry/core/plotting/plotter.py index cd6c69a011..18ce273c39 100644 --- a/src/ansys/geometry/core/plotting/plotter.py +++ b/src/ansys/geometry/core/plotting/plotter.py @@ -22,7 +22,7 @@ """Provides plotting for various PyAnsys Geometry objects.""" import re -from beartype.typing import Any, Dict, List, Optional +from beartype.typing import Any, Dict, List, Optional, Union import numpy as np import pyvista as pv from pyvista.plotting.plotter import Plotter as PyVistaPlotter @@ -231,6 +231,10 @@ def plot_sketch( if show_frame: self.plot_frame(sketch._plane) + if "clipping_plane" in plotting_options: + logger.warning("Clipping is not available in Sketch objects.") + plotting_options.pop("clipping_plane") + self.add_sketch_polydata(sketch.sketch_polydata_faces(), opacity=0.7, **plotting_options) self.add_sketch_polydata(sketch.sketch_polydata_edges(), **plotting_options) @@ -281,6 +285,9 @@ def add_body( # Use the default PyAnsys Geometry add_mesh arguments self.__set_add_mesh_defaults(plotting_options) dataset = body.tessellate(merge=merge) + if "clipping_plane" in plotting_options: + dataset = self.clip(dataset, plotting_options.get("clipping_plane")) + plotting_options.pop("clipping_plane", None) if isinstance(dataset, pv.MultiBlock): actor, _ = self.scene.add_composite(dataset, **plotting_options) else: @@ -324,6 +331,11 @@ def add_component( # Use the default PyAnsys Geometry add_mesh arguments self.__set_add_mesh_defaults(plotting_options) dataset = component.tessellate(merge_component=merge_component, merge_bodies=merge_bodies) + + if "clipping_plane" in plotting_options: + dataset = self.clip(dataset, plotting_options["clipping_plane"]) + plotting_options.pop("clipping_plane", None) + if isinstance(dataset, pv.MultiBlock): actor, _ = self.scene.add_composite(dataset, **plotting_options) else: @@ -344,8 +356,38 @@ def add_sketch_polydata(self, polydata_entries: List[pv.PolyData], **plotting_op :meth:`Plotter.add_mesh ` method. """ # Use the default PyAnsys Geometry add_mesh arguments + mb = pv.MultiBlock() for polydata in polydata_entries: - self.scene.add_mesh(polydata, color=EDGE_COLOR, **plotting_options) + mb.append(polydata) + + if "clipping_plane" in plotting_options: + mb = self.clip(mb, plane=plotting_options["clipping_plane"]) + plotting_options.pop("clipping_plane", None) + + self.scene.add_mesh(mb, color=EDGE_COLOR, **plotting_options) + + def clip( + self, mesh: Union[pv.PolyData, pv.MultiBlock], plane: Plane = None + ) -> Union[pv.PolyData, pv.MultiBlock]: + """ + Clip the passed mesh with a plane. + + Parameters + ---------- + mesh : Union[pv.PolyData, pv.MultiBlock] + Mesh you want to clip. + normal : str, optional + Plane you want to use for clipping, by default "x". + Available options: ["x", "-x", "y", "-y", "z", "-z"] + origin : tuple, optional + Origin point of the plane, by default None + + Returns + ------- + Union[pv.PolyData,pv.MultiBlock] + The clipped mesh. + """ + return mesh.clip(normal=plane.normal, origin=plane.origin) def add_design_point(self, design_point: DesignPoint, **plotting_options) -> None: """ @@ -369,7 +411,7 @@ def add( merge_components: bool = False, filter: str = None, **plotting_options, - ) -> Dict[pv.Actor, GeomObjectPlot]: + ) -> None: """ Add any type of object to the scene. @@ -393,11 +435,6 @@ def add( **plotting_options : dict, default: None Keyword arguments. For allowable keyword arguments, see the :meth:`Plotter.add_mesh ` method. - - Returns - ------- - Dict[~pyvista.Actor, GeomObjectPlot] - Mapping between the ~pyvista.Actor and the PyAnsys Geometry object. """ logger.debug(f"Adding object type {type(object)} to the PyVista plotter") if filter: @@ -410,8 +447,14 @@ def add( if isinstance(object, List) and isinstance(object[0], pv.PolyData): self.add_sketch_polydata(object, **plotting_options) elif isinstance(object, pv.PolyData): + if "clipping_plane" in plotting_options: + object = self.clip(object, plotting_options["clipping_plane"]) + plotting_options.pop("clipping_plane", None) self.scene.add_mesh(object, **plotting_options) elif isinstance(object, pv.MultiBlock): + if "clipping_plane" in plotting_options: + object = self.clip(object, plotting_options["clipping_plane"]) + plotting_options.pop("clipping_plane", None) self.scene.add_composite(object, **plotting_options) elif isinstance(object, Sketch): self.plot_sketch(object, **plotting_options) @@ -423,7 +466,6 @@ def add( self.add_design_point(object, **plotting_options) else: logger.warning(f"Object type {type(object)} can not be plotted.") - return self._geom_object_actors_map def add_list( self, @@ -432,7 +474,7 @@ def add_list( merge_components: bool = False, filter: str = None, **plotting_options, - ) -> Dict[pv.Actor, GeomObjectPlot]: + ) -> None: """ Add a list of any type of object to the scene. @@ -456,15 +498,9 @@ def add_list( **plotting_options : dict, default: None Keyword arguments. For allowable keyword arguments, see the :meth:`Plotter.add_mesh ` method. - - Returns - ------- - Dict[~pyvista.Actor, GeomObjectPlot] - Mapping between the ~pyvista.Actor and the PyAnsys Geometry objects. """ for object in plotting_list: _ = self.add(object, merge_bodies, merge_components, filter, **plotting_options) - return self._geom_object_actors_map def show( self, @@ -523,3 +559,8 @@ def __set_add_mesh_defaults(self, plotting_options: Optional[Dict]) -> None: # This method should only be applied in 3D objects: bodies, components plotting_options.setdefault("smooth_shading", True) plotting_options.setdefault("color", DEFAULT_COLOR) + + @property + def geom_object_actors_map(self) -> Dict[pv.Actor, GeomObjectPlot]: + """Mapping between the ~pyvista.Actor and the PyAnsys Geometry objects.""" + return self._geom_object_actors_map diff --git a/src/ansys/geometry/core/plotting/plotter_helper.py b/src/ansys/geometry/core/plotting/plotter_helper.py index 1c4db8bf20..d2db663066 100644 --- a/src/ansys/geometry/core/plotting/plotter_helper.py +++ b/src/ansys/geometry/core/plotting/plotter_helper.py @@ -220,7 +220,7 @@ def picker_callback(self, actor: "pv.Actor") -> None: def compute_edge_object_map(self) -> Dict[pv.Actor, EdgePlot]: """ - Compute the mapping between plotter actors and EdgePlot objects. + Compute the mapping between plotter actors and ``EdgePlot`` objects. Returns ------- @@ -247,9 +247,20 @@ def disable_picking(self): """Disable picking capabilities in the plotter.""" self._pl.scene.disable_picking() + def add(self, object: Any, **plotting_options): + """ + Add a ``pyansys-geometry`` or ``PyVista`` object to the plotter. + + Parameters + ---------- + object : Any + Object you want to show. + """ + self._pl.add(object=object, **plotting_options) + def plot( self, - object: Any, + object: Any = None, screenshot: Optional[str] = None, merge_bodies: bool = False, merge_component: bool = False, @@ -265,7 +276,7 @@ def plot( Parameters ---------- - object : Any + object : Any, default: None Any object or list of objects that you want to plot. screenshot : str, default: None Path for saving a screenshot of the image that is being represented. @@ -292,13 +303,13 @@ def plot( """ if isinstance(object, List) and not isinstance(object[0], pv.PolyData): logger.debug("Plotting objects in list...") - self._geom_object_actors_map = self._pl.add_list( - object, merge_bodies, merge_component, filter, **plotting_options - ) + self._pl.add_list(object, merge_bodies, merge_component, filter, **plotting_options) else: - self._geom_object_actors_map = self._pl.add( - object, merge_bodies, merge_component, filter, **plotting_options - ) + self._pl.add(object, merge_bodies, merge_component, filter, **plotting_options) + if self._pl.geom_object_actors_map: + self._geom_object_actors_map = self._pl.geom_object_actors_map + else: + logger.warning("No actors added to the plotter.") self.compute_edge_object_map() # Compute mapping between the objects and its edges. diff --git a/tests/integration/test_plotter.py b/tests/integration/test_plotter.py index 090de869da..3c949118fb 100644 --- a/tests/integration/test_plotter.py +++ b/tests/integration/test_plotter.py @@ -705,3 +705,46 @@ def test_plot_design_point(modeler: Modeler, verify_image_cache): plot_list, screenshot=Path(IMAGE_RESULTS_DIR, "test_plot_design_point.png"), ) + + +def test_plot_clipping(modeler: Modeler, verify_image_cache): + design = modeler.create_design("Clipping") + ph = PlotterHelper() + + plot_list = [] + # Create a Body cylinder + cylinder = Sketch() + cylinder.circle(Point2D([10, 10], UNITS.m), 1.0) + cylinder_body = design.extrude_sketch("JustACyl", cylinder, Quantity(10, UNITS.m)) + + origin = Point3D([10.0, 10.0, 5.0], UNITS.m) + plane = Plane(origin=origin, direction_x=[0, 0, 1], direction_y=[0, 1, 0]) + ph.add(cylinder_body, clipping_plane=plane) + + origin = Point3D([10.0, 10.0, 5.0], UNITS.m) + plane = Plane(origin=origin, direction_x=[0, 0, 1], direction_y=[0, 1, 0]) + ph.add(cylinder, clipping_plane=plane) + # Create a Body box + box2 = Sketch() + box2.box(Point2D([-10, 20], UNITS.m), Quantity(10, UNITS.m), Quantity(10, UNITS.m)) + box_body2 = design.extrude_sketch("JustABox", box2, Quantity(10, UNITS.m)) + + origin = Point3D([-10.0, 20.0, 5.0], UNITS.m) + plane = Plane(origin=origin, direction_x=[1, 1, 1], direction_y=[-1, 0, 1]) + ph.add(box_body2, clipping_plane=plane) + + origin = Point3D([0, 0, 0], UNITS.m) + plane = Plane(origin=origin, direction_x=[1, 1, 1], direction_y=[-1, 0, 1]) + sphere = pv.Sphere() + ph.add(sphere, clipping_plane=plane) + + origin = Point3D([5, -10, 10], UNITS.m) + plane = Plane(origin=origin, direction_x=[1, 1, 1], direction_y=[-1, 0, 1]) + sphere = pv.Sphere(center=(5, -10, -10)) + ph.add(pv.MultiBlock([sphere]), clipping_plane=plane) + + sphere1 = pv.Sphere(center=(-5, -10, -10)) + sphere2 = pv.Sphere(center=(-10, -10, -10)) + ph.add([sphere1, sphere2], clipping_plane=plane) + + ph.plot()