Skip to content

Commit

Permalink
feat: add plane clipping capabilities to plotter (#774)
Browse files Browse the repository at this point in the history
Co-authored-by: Roberto Pastor Muela <[email protected]>
  • Loading branch information
AlejandroFernandezLuces and RobPasMue authored Nov 24, 2023
1 parent 2e069bc commit ebbef32
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 25 deletions.
12 changes: 12 additions & 0 deletions src/ansys/geometry/core/math/plane.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
73 changes: 57 additions & 16 deletions src/ansys/geometry/core/plotting/plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -344,8 +356,38 @@ def add_sketch_polydata(self, polydata_entries: List[pv.PolyData], **plotting_op
:meth:`Plotter.add_mesh <pyvista.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:
"""
Expand All @@ -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.
Expand All @@ -393,11 +435,6 @@ def add(
**plotting_options : dict, default: None
Keyword arguments. For allowable keyword arguments, see the
:meth:`Plotter.add_mesh <pyvista.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:
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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.
Expand All @@ -456,15 +498,9 @@ def add_list(
**plotting_options : dict, default: None
Keyword arguments. For allowable keyword arguments, see the
:meth:`Plotter.add_mesh <pyvista.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,
Expand Down Expand Up @@ -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
29 changes: 20 additions & 9 deletions src/ansys/geometry/core/plotting/plotter_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------
Expand All @@ -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,
Expand All @@ -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.
Expand All @@ -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.
Expand Down
43 changes: 43 additions & 0 deletions tests/integration/test_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

0 comments on commit ebbef32

Please sign in to comment.