Skip to content

Commit 9b29316

Browse files
feat: add plane clipping capabilities to plotter (#774)
Co-authored-by: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com>
1 parent 7e712b4 commit 9b29316

File tree

4 files changed

+132
-25
lines changed

4 files changed

+132
-25
lines changed

src/ansys/geometry/core/math/plane.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ def is_point_contained(self, point: Point3D) -> bool:
8181
# If plane equation is equal to 0, your point is contained
8282
return True if np.isclose(plane_eq, 0.0) else False
8383

84+
@property
85+
def normal(self) -> UnitVector3D:
86+
"""
87+
Calculate the normal vector of the plane.
88+
89+
Returns
90+
-------
91+
UnitVector3D
92+
Normal vector of the plane.
93+
"""
94+
return self.direction_z
95+
8496
@check_input_types
8597
def __eq__(self, other: "Plane") -> bool:
8698
"""Equals operator for the ``Plane`` class."""

src/ansys/geometry/core/plotting/plotter.py

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"""Provides plotting for various PyAnsys Geometry objects."""
2323
import re
2424

25-
from beartype.typing import Any, Dict, List, Optional
25+
from beartype.typing import Any, Dict, List, Optional, Union
2626
import numpy as np
2727
import pyvista as pv
2828
from pyvista.plotting.plotter import Plotter as PyVistaPlotter
@@ -231,6 +231,10 @@ def plot_sketch(
231231
if show_frame:
232232
self.plot_frame(sketch._plane)
233233

234+
if "clipping_plane" in plotting_options:
235+
logger.warning("Clipping is not available in Sketch objects.")
236+
plotting_options.pop("clipping_plane")
237+
234238
self.add_sketch_polydata(sketch.sketch_polydata_faces(), opacity=0.7, **plotting_options)
235239
self.add_sketch_polydata(sketch.sketch_polydata_edges(), **plotting_options)
236240

@@ -281,6 +285,9 @@ def add_body(
281285
# Use the default PyAnsys Geometry add_mesh arguments
282286
self.__set_add_mesh_defaults(plotting_options)
283287
dataset = body.tessellate(merge=merge)
288+
if "clipping_plane" in plotting_options:
289+
dataset = self.clip(dataset, plotting_options.get("clipping_plane"))
290+
plotting_options.pop("clipping_plane", None)
284291
if isinstance(dataset, pv.MultiBlock):
285292
actor, _ = self.scene.add_composite(dataset, **plotting_options)
286293
else:
@@ -324,6 +331,11 @@ def add_component(
324331
# Use the default PyAnsys Geometry add_mesh arguments
325332
self.__set_add_mesh_defaults(plotting_options)
326333
dataset = component.tessellate(merge_component=merge_component, merge_bodies=merge_bodies)
334+
335+
if "clipping_plane" in plotting_options:
336+
dataset = self.clip(dataset, plotting_options["clipping_plane"])
337+
plotting_options.pop("clipping_plane", None)
338+
327339
if isinstance(dataset, pv.MultiBlock):
328340
actor, _ = self.scene.add_composite(dataset, **plotting_options)
329341
else:
@@ -344,8 +356,38 @@ def add_sketch_polydata(self, polydata_entries: List[pv.PolyData], **plotting_op
344356
:meth:`Plotter.add_mesh <pyvista.Plotter.add_mesh>` method.
345357
"""
346358
# Use the default PyAnsys Geometry add_mesh arguments
359+
mb = pv.MultiBlock()
347360
for polydata in polydata_entries:
348-
self.scene.add_mesh(polydata, color=EDGE_COLOR, **plotting_options)
361+
mb.append(polydata)
362+
363+
if "clipping_plane" in plotting_options:
364+
mb = self.clip(mb, plane=plotting_options["clipping_plane"])
365+
plotting_options.pop("clipping_plane", None)
366+
367+
self.scene.add_mesh(mb, color=EDGE_COLOR, **plotting_options)
368+
369+
def clip(
370+
self, mesh: Union[pv.PolyData, pv.MultiBlock], plane: Plane = None
371+
) -> Union[pv.PolyData, pv.MultiBlock]:
372+
"""
373+
Clip the passed mesh with a plane.
374+
375+
Parameters
376+
----------
377+
mesh : Union[pv.PolyData, pv.MultiBlock]
378+
Mesh you want to clip.
379+
normal : str, optional
380+
Plane you want to use for clipping, by default "x".
381+
Available options: ["x", "-x", "y", "-y", "z", "-z"]
382+
origin : tuple, optional
383+
Origin point of the plane, by default None
384+
385+
Returns
386+
-------
387+
Union[pv.PolyData,pv.MultiBlock]
388+
The clipped mesh.
389+
"""
390+
return mesh.clip(normal=plane.normal, origin=plane.origin)
349391

350392
def add_design_point(self, design_point: DesignPoint, **plotting_options) -> None:
351393
"""
@@ -369,7 +411,7 @@ def add(
369411
merge_components: bool = False,
370412
filter: str = None,
371413
**plotting_options,
372-
) -> Dict[pv.Actor, GeomObjectPlot]:
414+
) -> None:
373415
"""
374416
Add any type of object to the scene.
375417
@@ -393,11 +435,6 @@ def add(
393435
**plotting_options : dict, default: None
394436
Keyword arguments. For allowable keyword arguments, see the
395437
:meth:`Plotter.add_mesh <pyvista.Plotter.add_mesh>` method.
396-
397-
Returns
398-
-------
399-
Dict[~pyvista.Actor, GeomObjectPlot]
400-
Mapping between the ~pyvista.Actor and the PyAnsys Geometry object.
401438
"""
402439
logger.debug(f"Adding object type {type(object)} to the PyVista plotter")
403440
if filter:
@@ -410,8 +447,14 @@ def add(
410447
if isinstance(object, List) and isinstance(object[0], pv.PolyData):
411448
self.add_sketch_polydata(object, **plotting_options)
412449
elif isinstance(object, pv.PolyData):
450+
if "clipping_plane" in plotting_options:
451+
object = self.clip(object, plotting_options["clipping_plane"])
452+
plotting_options.pop("clipping_plane", None)
413453
self.scene.add_mesh(object, **plotting_options)
414454
elif isinstance(object, pv.MultiBlock):
455+
if "clipping_plane" in plotting_options:
456+
object = self.clip(object, plotting_options["clipping_plane"])
457+
plotting_options.pop("clipping_plane", None)
415458
self.scene.add_composite(object, **plotting_options)
416459
elif isinstance(object, Sketch):
417460
self.plot_sketch(object, **plotting_options)
@@ -423,7 +466,6 @@ def add(
423466
self.add_design_point(object, **plotting_options)
424467
else:
425468
logger.warning(f"Object type {type(object)} can not be plotted.")
426-
return self._geom_object_actors_map
427469

428470
def add_list(
429471
self,
@@ -432,7 +474,7 @@ def add_list(
432474
merge_components: bool = False,
433475
filter: str = None,
434476
**plotting_options,
435-
) -> Dict[pv.Actor, GeomObjectPlot]:
477+
) -> None:
436478
"""
437479
Add a list of any type of object to the scene.
438480
@@ -456,15 +498,9 @@ def add_list(
456498
**plotting_options : dict, default: None
457499
Keyword arguments. For allowable keyword arguments, see the
458500
:meth:`Plotter.add_mesh <pyvista.Plotter.add_mesh>` method.
459-
460-
Returns
461-
-------
462-
Dict[~pyvista.Actor, GeomObjectPlot]
463-
Mapping between the ~pyvista.Actor and the PyAnsys Geometry objects.
464501
"""
465502
for object in plotting_list:
466503
_ = self.add(object, merge_bodies, merge_components, filter, **plotting_options)
467-
return self._geom_object_actors_map
468504

469505
def show(
470506
self,
@@ -523,3 +559,8 @@ def __set_add_mesh_defaults(self, plotting_options: Optional[Dict]) -> None:
523559
# This method should only be applied in 3D objects: bodies, components
524560
plotting_options.setdefault("smooth_shading", True)
525561
plotting_options.setdefault("color", DEFAULT_COLOR)
562+
563+
@property
564+
def geom_object_actors_map(self) -> Dict[pv.Actor, GeomObjectPlot]:
565+
"""Mapping between the ~pyvista.Actor and the PyAnsys Geometry objects."""
566+
return self._geom_object_actors_map

src/ansys/geometry/core/plotting/plotter_helper.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def picker_callback(self, actor: "pv.Actor") -> None:
220220

221221
def compute_edge_object_map(self) -> Dict[pv.Actor, EdgePlot]:
222222
"""
223-
Compute the mapping between plotter actors and EdgePlot objects.
223+
Compute the mapping between plotter actors and ``EdgePlot`` objects.
224224
225225
Returns
226226
-------
@@ -247,9 +247,20 @@ def disable_picking(self):
247247
"""Disable picking capabilities in the plotter."""
248248
self._pl.scene.disable_picking()
249249

250+
def add(self, object: Any, **plotting_options):
251+
"""
252+
Add a ``pyansys-geometry`` or ``PyVista`` object to the plotter.
253+
254+
Parameters
255+
----------
256+
object : Any
257+
Object you want to show.
258+
"""
259+
self._pl.add(object=object, **plotting_options)
260+
250261
def plot(
251262
self,
252-
object: Any,
263+
object: Any = None,
253264
screenshot: Optional[str] = None,
254265
merge_bodies: bool = False,
255266
merge_component: bool = False,
@@ -265,7 +276,7 @@ def plot(
265276
266277
Parameters
267278
----------
268-
object : Any
279+
object : Any, default: None
269280
Any object or list of objects that you want to plot.
270281
screenshot : str, default: None
271282
Path for saving a screenshot of the image that is being represented.
@@ -292,13 +303,13 @@ def plot(
292303
"""
293304
if isinstance(object, List) and not isinstance(object[0], pv.PolyData):
294305
logger.debug("Plotting objects in list...")
295-
self._geom_object_actors_map = self._pl.add_list(
296-
object, merge_bodies, merge_component, filter, **plotting_options
297-
)
306+
self._pl.add_list(object, merge_bodies, merge_component, filter, **plotting_options)
298307
else:
299-
self._geom_object_actors_map = self._pl.add(
300-
object, merge_bodies, merge_component, filter, **plotting_options
301-
)
308+
self._pl.add(object, merge_bodies, merge_component, filter, **plotting_options)
309+
if self._pl.geom_object_actors_map:
310+
self._geom_object_actors_map = self._pl.geom_object_actors_map
311+
else:
312+
logger.warning("No actors added to the plotter.")
302313

303314
self.compute_edge_object_map()
304315
# Compute mapping between the objects and its edges.

tests/integration/test_plotter.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,3 +705,46 @@ def test_plot_design_point(modeler: Modeler, verify_image_cache):
705705
plot_list,
706706
screenshot=Path(IMAGE_RESULTS_DIR, "test_plot_design_point.png"),
707707
)
708+
709+
710+
def test_plot_clipping(modeler: Modeler, verify_image_cache):
711+
design = modeler.create_design("Clipping")
712+
ph = PlotterHelper()
713+
714+
plot_list = []
715+
# Create a Body cylinder
716+
cylinder = Sketch()
717+
cylinder.circle(Point2D([10, 10], UNITS.m), 1.0)
718+
cylinder_body = design.extrude_sketch("JustACyl", cylinder, Quantity(10, UNITS.m))
719+
720+
origin = Point3D([10.0, 10.0, 5.0], UNITS.m)
721+
plane = Plane(origin=origin, direction_x=[0, 0, 1], direction_y=[0, 1, 0])
722+
ph.add(cylinder_body, clipping_plane=plane)
723+
724+
origin = Point3D([10.0, 10.0, 5.0], UNITS.m)
725+
plane = Plane(origin=origin, direction_x=[0, 0, 1], direction_y=[0, 1, 0])
726+
ph.add(cylinder, clipping_plane=plane)
727+
# Create a Body box
728+
box2 = Sketch()
729+
box2.box(Point2D([-10, 20], UNITS.m), Quantity(10, UNITS.m), Quantity(10, UNITS.m))
730+
box_body2 = design.extrude_sketch("JustABox", box2, Quantity(10, UNITS.m))
731+
732+
origin = Point3D([-10.0, 20.0, 5.0], UNITS.m)
733+
plane = Plane(origin=origin, direction_x=[1, 1, 1], direction_y=[-1, 0, 1])
734+
ph.add(box_body2, clipping_plane=plane)
735+
736+
origin = Point3D([0, 0, 0], UNITS.m)
737+
plane = Plane(origin=origin, direction_x=[1, 1, 1], direction_y=[-1, 0, 1])
738+
sphere = pv.Sphere()
739+
ph.add(sphere, clipping_plane=plane)
740+
741+
origin = Point3D([5, -10, 10], UNITS.m)
742+
plane = Plane(origin=origin, direction_x=[1, 1, 1], direction_y=[-1, 0, 1])
743+
sphere = pv.Sphere(center=(5, -10, -10))
744+
ph.add(pv.MultiBlock([sphere]), clipping_plane=plane)
745+
746+
sphere1 = pv.Sphere(center=(-5, -10, -10))
747+
sphere2 = pv.Sphere(center=(-10, -10, -10))
748+
ph.add([sphere1, sphere2], clipping_plane=plane)
749+
750+
ph.plot()

0 commit comments

Comments
 (0)