Skip to content

Commit 5bd252a

Browse files
RobPasMuepyansys-ci-botMaxJPRey
authored
feat: allow plotting of individual faces (#1757)
Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com>
1 parent fd2a460 commit 5bd252a

File tree

5 files changed

+180
-14
lines changed

5 files changed

+180
-14
lines changed

doc/changelog.d/1757.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
allow plotting of individual faces

src/ansys/geometry/core/designer/body.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,9 +1223,12 @@ def tessellate( # noqa: D102
12231223
# cache tessellation
12241224
if not self._tessellation:
12251225
resp = self._bodies_stub.GetTessellation(self._grpc_id)
1226-
self._tessellation = resp.face_tessellation.values()
1226+
self._tessellation = {
1227+
str(face_id): tess_to_pd(face_tess)
1228+
for face_id, face_tess in resp.face_tessellation.items()
1229+
}
12271230

1228-
pdata = [tess_to_pd(tess).transform(transform) for tess in self._tessellation]
1231+
pdata = [tess.transform(transform, inplace=False) for tess in self._tessellation.values()]
12291232
comp = pv.MultiBlock(pdata)
12301233

12311234
if merge:

src/ansys/geometry/core/designer/face.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
from ansys.tools.visualization_interface.utils.color import Color
6666

6767
if TYPE_CHECKING: # pragma: no cover
68+
import pyvista as pv
69+
6870
from ansys.geometry.core.designer.body import Body
6971

7072

@@ -564,3 +566,70 @@ def setup_offset_relationship(
564566
)
565567

566568
return result.success
569+
570+
def tessellate(self) -> "pv.PolyData":
571+
"""Tessellate the face and return the geometry as triangles.
572+
573+
Returns
574+
-------
575+
~pyvista.PolyData
576+
:class:`pyvista.PolyData` object holding the face.
577+
"""
578+
# If tessellation has not been called before... call it
579+
if self._body._template._tessellation is None:
580+
self._body.tessellate()
581+
582+
# Search the tessellation of the face - if it exists
583+
# ---> We need to used the last element of the ID since we are looking inside
584+
# ---> the master body tessellation.
585+
red_id = self.id.split("/")[-1]
586+
mb_pdata = self.body._template._tessellation.get(red_id)
587+
if mb_pdata is None: # pragma: no cover
588+
raise ValueError(f"Face {self.id} not found in the tessellation.")
589+
590+
# Return the stored PolyData
591+
return mb_pdata.transform(self.body.parent_component.get_world_transform(), inplace=False)
592+
593+
def plot(
594+
self,
595+
screenshot: str | None = None,
596+
use_trame: bool | None = None,
597+
use_service_colors: bool | None = None,
598+
**plotting_options: dict | None,
599+
) -> None:
600+
"""Plot the face.
601+
602+
Parameters
603+
----------
604+
screenshot : str, default: None
605+
Path for saving a screenshot of the image that is being represented.
606+
use_trame : bool, default: None
607+
Whether to enable the use of `trame <https://kitware.github.io/trame/index.html>`_.
608+
The default is ``None``, in which case the
609+
``ansys.tools.visualization_interface.USE_TRAME`` global setting is used.
610+
use_service_colors : bool, default: None
611+
Whether to use the colors assigned to the face in the service. The default
612+
is ``None``, in which case the ``ansys.geometry.core.USE_SERVICE_COLORS``
613+
global setting is used.
614+
**plotting_options : dict, default: None
615+
Keyword arguments for plotting. For allowable keyword arguments, see the
616+
:meth:`Plotter.add_mesh <pyvista.Plotter.add_mesh>` method.
617+
618+
"""
619+
# lazy import here to improve initial module loading time
620+
import ansys.geometry.core as pyansys_geometry
621+
from ansys.geometry.core.plotting import GeometryPlotter
622+
from ansys.tools.visualization_interface.types.mesh_object_plot import (
623+
MeshObjectPlot,
624+
)
625+
626+
use_service_colors = (
627+
use_service_colors
628+
if use_service_colors is not None
629+
else pyansys_geometry.USE_SERVICE_COLORS
630+
)
631+
632+
mesh_object = self if use_service_colors else MeshObjectPlot(self, self.tessellate())
633+
pl = GeometryPlotter(use_trame=use_trame, use_service_colors=use_service_colors)
634+
pl.plot(mesh_object, **plotting_options)
635+
pl.show(screenshot=screenshot, **plotting_options)

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

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from ansys.geometry.core.designer.component import Component
3636
from ansys.geometry.core.designer.design import Design
3737
from ansys.geometry.core.designer.designpoint import DesignPoint
38+
from ansys.geometry.core.designer.face import Face
3839
from ansys.geometry.core.logger import LOG
3940
from ansys.geometry.core.math.frame import Frame
4041
from ansys.geometry.core.math.plane import Plane
@@ -243,21 +244,18 @@ def add_body(self, body: Body, merge: bool = False, **plotting_options: dict | N
243244

244245
if self.use_service_colors:
245246
faces = body.faces
246-
dataset = body.tessellate(merge=merge)
247247
body_color = body.color
248248
if not merge:
249-
# ASSUMPTION: the faces returned by the service are in the same order
250-
# as the tessellation information returned by the service...
251-
elems = [elem for elem in dataset]
252-
for face, block in zip(faces, elems):
249+
for face in faces:
253250
face_color = face.color
254251
if face_color != Color.DEFAULT.value:
255252
plotting_options["color"] = face_color
256253
else:
257254
plotting_options["color"] = body_color
258-
self._backend.pv_interface.plot(block, **plotting_options)
255+
self._backend.pv_interface.plot(face.tessellate(), **plotting_options)
259256
return
260257
else:
258+
dataset = body.tessellate(merge=True)
261259
plotting_options["color"] = body_color
262260
self._backend.pv_interface.plot(dataset, **plotting_options)
263261
return
@@ -281,6 +279,32 @@ def add_body(self, body: Body, merge: bool = False, **plotting_options: dict | N
281279
if self._backend._allow_picking:
282280
self.add_body_edges(body_plot)
283281

282+
def add_face(self, face: Face, **plotting_options: dict | None) -> None:
283+
"""Add a face to the scene.
284+
285+
Parameters
286+
----------
287+
face : Face
288+
Face to add.
289+
**plotting_options : dict, default: None
290+
Keyword arguments. For allowable keyword arguments, see the
291+
:meth:`Plotter.add_mesh <pyvista.Plotter.add_mesh>` method.
292+
"""
293+
self._backend.pv_interface.set_add_mesh_defaults(plotting_options)
294+
dataset = face.tessellate()
295+
if self.use_service_colors:
296+
face_color = face.color
297+
if face_color != Color.DEFAULT.value:
298+
plotting_options["color"] = face_color
299+
else:
300+
plotting_options["color"] = face.body.color
301+
self._backend.pv_interface.plot(dataset, **plotting_options)
302+
return
303+
304+
# Otherwise...
305+
face_plot = MeshObjectPlot(custom_object=face, mesh=dataset)
306+
self._backend.pv_interface.plot(face_plot, **plotting_options)
307+
284308
def add_component(
285309
self,
286310
component: Component,
@@ -438,6 +462,8 @@ def plot(self, plottable_object: Any, name_filter: str = None, **plotting_option
438462
self.add_sketch(plottable_object, **plotting_options)
439463
elif isinstance(plottable_object, (Body, MasterBody)):
440464
self.add_body(plottable_object, merge_bodies, **plotting_options)
465+
elif isinstance(plottable_object, Face):
466+
self.add_face(plottable_object, **plotting_options)
441467
elif isinstance(plottable_object, (Design, Component)):
442468
self.add_component(plottable_object, merge_component, merge_bodies, **plotting_options)
443469
elif (
@@ -492,7 +518,7 @@ def show(
492518

493519
def export_glb(
494520
self, plotting_object: Any = None, filename: str | Path | None = None, **plotting_options
495-
) -> None:
521+
) -> Path:
496522
"""Export the design to a glb file. Does not support picked objects.
497523
498524
Parameters

tests/integration/test_plotter.py

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,7 @@ def test_export_glb(modeler: Modeler, verify_image_cache):
936936
sketch.box(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(10, UNITS.mm))
937937

938938
# Create your design on the server side
939-
design = modeler.create_design("BoxExtrusions")
939+
design = modeler.create_design("GLBBox")
940940

941941
# Extrude the sketch to create a body
942942
box_body = design.extrude_sketch("JustABox", sketch, Quantity(10, UNITS.mm))
@@ -956,7 +956,7 @@ def test_export_glb_with_color(modeler: Modeler, verify_image_cache):
956956
sketch.box(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(10, UNITS.mm))
957957

958958
# Create your design on the server side
959-
design = modeler.create_design("BoxExtrusions")
959+
design = modeler.create_design("GLBBoxWithColor")
960960

961961
# Extrude the sketch to create a body
962962
box_body = design.extrude_sketch("JustABox", sketch, Quantity(10, UNITS.mm))
@@ -970,14 +970,14 @@ def test_export_glb_with_color(modeler: Modeler, verify_image_cache):
970970

971971

972972
@skip_no_xserver
973-
def test_export_glb_with_face_color(modeler: Modeler, verify_image_cache):
973+
def test_export_glb_with_face_color(modeler: Modeler):
974974
"""Test exporting a box to glb."""
975975
# Create a Sketch
976976
sketch = Sketch()
977977
sketch.box(Point2D([10, 10], UNITS.m), Quantity(10, UNITS.m), Quantity(10, UNITS.m))
978978

979979
# Create your design on the server side
980-
design = modeler.create_design("BoxExtrusions")
980+
design = modeler.create_design("GLBBoxWithFaceColors")
981981

982982
# Extrude the sketch to create a body
983983
box_body = design.extrude_sketch("JustABox", sketch, Quantity(10, UNITS.m))
@@ -995,7 +995,7 @@ def test_export_glb_with_face_color(modeler: Modeler, verify_image_cache):
995995
def test_export_glb_cylinder_with_face_color(modeler: Modeler, verify_image_cache):
996996
"""Test exporting a cylinder to glb."""
997997
# Create your design on the server side
998-
design = modeler.create_design("BoxExtrusions")
998+
design = modeler.create_design("GLBCylinderWithFaceColors")
999999

10001000
# Create a sketch of a circle (overlapping the box slightly)
10011001
sketch_circle = Sketch().circle(Point2D([20, 0], unit=UNITS.m), radius=3 * UNITS.m)
@@ -1009,3 +1009,70 @@ def test_export_glb_cylinder_with_face_color(modeler: Modeler, verify_image_cach
10091009

10101010
output_glb_path = Path(IMAGE_RESULTS_DIR, "plot_cylinder_glb_face_colored")
10111011
pl.export_glb(cyl, filename=output_glb_path)
1012+
1013+
1014+
@skip_no_xserver
1015+
def test_plot_face_colors_from_service(modeler: Modeler, verify_image_cache):
1016+
"""Test exporting a box to glb."""
1017+
# Create a Sketch
1018+
sketch = Sketch()
1019+
sketch.box(Point2D([10, 10], UNITS.m), Quantity(10, UNITS.m), Quantity(10, UNITS.m))
1020+
1021+
# Create your design on the server side
1022+
design = modeler.create_design("BoxWithColors")
1023+
1024+
# Extrude the sketch to create a body
1025+
box_body = design.extrude_sketch("JustABox", sketch, Quantity(10, UNITS.m))
1026+
# Let's assign colors to the faces...
1027+
# 1) Box at large
1028+
box_body.set_color((255, 0, 0))
1029+
# 2) +Z face
1030+
box_body.faces[1].set_color((0, 255, 0))
1031+
# 3) +X face
1032+
box_body.faces[2].set_color((0, 0, 255))
1033+
1034+
box_body.plot(
1035+
screenshot=Path(IMAGE_RESULTS_DIR, "test_plot_face_colors_from_service.png"),
1036+
use_service_colors=True,
1037+
)
1038+
1039+
1040+
@skip_no_xserver
1041+
def test_plot_single_face(modeler: Modeler, verify_image_cache):
1042+
"""Test plotting a single face."""
1043+
# Create your design on the server side
1044+
design = modeler.create_design("SingleFace")
1045+
1046+
# Create a sketch of a box
1047+
sketch = Sketch()
1048+
sketch.box(Point2D([10, 10], UNITS.m), Quantity(10, UNITS.m), Quantity(10, UNITS.m))
1049+
1050+
# Extrude the sketch to create a body
1051+
box_body = design.extrude_sketch("JustABox", sketch, Quantity(10, UNITS.m))
1052+
1053+
# Test the plotting of the body
1054+
box_body.faces[1].plot(screenshot=Path(IMAGE_RESULTS_DIR, "plot_single_face.png"))
1055+
1056+
1057+
@skip_no_xserver
1058+
def test_plot_single_face_with_service_color(
1059+
modeler: Modeler, use_service_colors: None, verify_image_cache
1060+
):
1061+
"""Test plotting a single face with service colors."""
1062+
# Create your design on the server side
1063+
design = modeler.create_design("SingleFaceWithServiceColor")
1064+
1065+
# Create a sketch of a box
1066+
sketch = Sketch()
1067+
sketch.box(Point2D([10, 10], UNITS.m), Quantity(10, UNITS.m), Quantity(10, UNITS.m))
1068+
1069+
# Extrude the sketch to create a body
1070+
box_body = design.extrude_sketch("JustABox", sketch, Quantity(10, UNITS.m))
1071+
1072+
# Assign color to the body
1073+
box_body.color = "red"
1074+
1075+
# Test the plotting of the body
1076+
box_body.faces[1].plot(
1077+
screenshot=Path(IMAGE_RESULTS_DIR, "plot_single_face_with_service_color.png")
1078+
)

0 commit comments

Comments
 (0)