From 7f3386e9e05f6d0594221ad0184226484eb8eb1a Mon Sep 17 00:00:00 2001 From: Martin Walters <104021577+waltersma@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:57:24 +0000 Subject: [PATCH 01/11] feat: add quadratic face rendering --- src/ansys/meshing/prime/core/mesh.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ansys/meshing/prime/core/mesh.py b/src/ansys/meshing/prime/core/mesh.py index 986bd356da..8ed8e6cdf7 100644 --- a/src/ansys/meshing/prime/core/mesh.py +++ b/src/ansys/meshing/prime/core/mesh.py @@ -323,6 +323,12 @@ def get_face_polydata( vertices, faces = self._get_vertices_and_surf_faces(face_facet_res, index) surf = pv.PolyData(vertices, faces) + # Extract original edges before triangulation to preserve polygon boundaries + original_edges = surf.extract_all_edges() + # Triangulate to improve rendering of non-planar polygons + surf = surf.triangulate() + # Store original edges for rendering + surf._original_edges = original_edges fcolor = np.array(self.get_face_color(part, ColorByType.ZONE)) colors = np.tile(fcolor, (surf.n_faces_strict, 1)) surf["colors"] = colors From 43fa6b7614a95ced069a6ac5b5b3245083e77a63 Mon Sep 17 00:00:00 2001 From: Martin Walters <104021577+waltersma@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:59:02 +0000 Subject: [PATCH 02/11] Update plotter.py --- src/ansys/meshing/prime/graphics/plotter.py | 56 ++++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/src/ansys/meshing/prime/graphics/plotter.py b/src/ansys/meshing/prime/graphics/plotter.py index 49fc689c57..1eddcffd55 100644 --- a/src/ansys/meshing/prime/graphics/plotter.py +++ b/src/ansys/meshing/prime/graphics/plotter.py @@ -73,15 +73,28 @@ class PrimePlotter(Plotter): Whether to use the Trame visualizer. allow_picking : Optional[bool], default: True. Whether to allow picking. + improved_surface_rendering : Optional[bool], default: True. + Whether to use improved rendering for non-planar polygon faces. + When True, surfaces are subdivided for accurate display of curved or + non-planar faces while preserving original mesh edges. This is + particularly useful for visualizing quadratic elements where mid-side + nodes create curved edges that would otherwise appear faceted. + When False, the original polygon mesh is rendered directly. """ def __init__( - self, use_trame: Optional[bool] = None, allow_picking: Optional[bool] = True + self, + use_trame: Optional[bool] = None, + allow_picking: Optional[bool] = True, + improved_surface_rendering: Optional[bool] = True, ) -> None: """Initialize the widget.""" self._backend = PyVistaBackend(use_trame=use_trame, allow_picking=allow_picking) super().__init__(backend=self._backend) + # Store rendering preference for non-planar surfaces + self._improved_surface_rendering = improved_surface_rendering + # info of the actor to pass to picked info widget self._info_actor_map = {} self._backend.add_widget(ToggleEdges(self)) @@ -170,12 +183,41 @@ def add_model_pd(self, model_pd: Dict) -> None: # but we need the actor for the picked info widget colors = self.get_scalar_colors(face_mesh_info) has_mesh = face_mesh_info.has_mesh - actor = self._backend.pv_interface.scene.add_mesh( - face_mesh_part.mesh, show_edges=has_mesh, color=colors, pickable=True - ) - face_mesh_part.actor = actor - self._backend.pv_interface._object_to_actors_map[actor] = face_mesh_part - self._info_actor_map[actor] = face_mesh_info + + if self._improved_surface_rendering: + # Render subdivided faces without edges (edges shown separately) + actor = self._backend.pv_interface.scene.add_mesh( + face_mesh_part.mesh, show_edges=False, color=colors, pickable=True + ) + # Apply polygon offset to push faces back in depth buffer + # This prevents z-fighting with edge lines + actor.GetProperty().SetEdgeVisibility(False) + actor.GetMapper().SetResolveCoincidentTopologyToPolygonOffset() + actor.GetMapper().SetRelativeCoincidentTopologyPolygonOffsetParameters( + 1.0, 1.0 + ) + face_mesh_part.actor = actor + self._backend.pv_interface._object_to_actors_map[actor] = face_mesh_part + self._info_actor_map[actor] = face_mesh_info + + # Render original polygon edges if mesh has edges and original edges exist + if has_mesh and hasattr(face_mesh_part.mesh, '_original_edges'): + original_edges = face_mesh_part.mesh._original_edges + if original_edges.n_points > 0: + self._backend.pv_interface.scene.add_mesh( + original_edges, + color='black', + line_width=1, + pickable=False, + ) + else: + # Original rendering approach without triangulation + actor = self._backend.pv_interface.scene.add_mesh( + face_mesh_part.mesh, show_edges=has_mesh, color=colors, pickable=True + ) + face_mesh_part.actor = actor + self._backend.pv_interface._object_to_actors_map[actor] = face_mesh_part + self._info_actor_map[actor] = face_mesh_info if "edges" in part_polydata.keys(): for edge_mesh_part in part_polydata["edges"]: From 53f60442c4414ae372ece1038e939290b065fbc1 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:00:07 +0000 Subject: [PATCH 03/11] chore: adding changelog file 1220.miscellaneous.md [dependabot-skip] --- doc/changelog.d/1220.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/1220.miscellaneous.md diff --git a/doc/changelog.d/1220.miscellaneous.md b/doc/changelog.d/1220.miscellaneous.md new file mode 100644 index 0000000000..325fbdb581 --- /dev/null +++ b/doc/changelog.d/1220.miscellaneous.md @@ -0,0 +1 @@ +Feat: add quadratic face rendering From 8daa7c3584486599bac1c2fa45b76f3602ab1822 Mon Sep 17 00:00:00 2001 From: Martin Walters <104021577+waltersma@users.noreply.github.com> Date: Fri, 30 Jan 2026 13:38:11 +0000 Subject: [PATCH 04/11] Update mesh.py --- src/ansys/meshing/prime/core/mesh.py | 101 +++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 12 deletions(-) diff --git a/src/ansys/meshing/prime/core/mesh.py b/src/ansys/meshing/prime/core/mesh.py index 8ed8e6cdf7..c2284cff4c 100644 --- a/src/ansys/meshing/prime/core/mesh.py +++ b/src/ansys/meshing/prime/core/mesh.py @@ -116,6 +116,79 @@ def __init__( self.has_mesh = has_mesh +class DisplayPolyData: + """Wrapper for PyVista PolyData with support for improved surface rendering. + + This class wraps a PyVista PolyData object and provides methods for accessing + the original (non-triangulated) mesh and its edges for improved rendering + of non-planar polygon faces and quadratic elements. + + Using composition rather than inheritance avoids potential conflicts with + PyVista's internal state management. + + Parameters + ---------- + mesh : pv.PolyData + The triangulated mesh for rendering. + original_mesh : pv.PolyData, optional + The original (non-triangulated) mesh for edge extraction. + """ + + def __init__(self, mesh: pv.PolyData, original_mesh: pv.PolyData = None): + """Initialize the display polydata.""" + self._mesh = mesh + self._original_polydata = original_mesh + self._cached_original_edges = None + + @property + def mesh(self) -> pv.PolyData: + """Get the triangulated mesh for rendering. + + Returns + ------- + pv.PolyData + The triangulated mesh. + """ + return self._mesh + + def get_original_polydata(self) -> pv.PolyData: + """Get the original (non-triangulated) polydata. + + Returns + ------- + pv.PolyData + The original polydata, or the triangulated mesh if not set. + """ + return self._original_polydata if self._original_polydata is not None else self._mesh + + def has_original_edges(self) -> bool: + """Check if original edges can be extracted. + + Returns + ------- + bool + True if original polydata is available for edge extraction. + """ + return self._original_polydata is not None + + def get_original_edges(self) -> pv.PolyData: + """Get the original polygon edges, lazily extracted and cached. + + This extracts edges from the original (non-triangulated) mesh, + which preserves the true element boundaries for display. + + Returns + ------- + pv.PolyData + The edges of the original mesh, or None if not available. + """ + if self._original_polydata is None: + return None + if self._cached_original_edges is None: + self._cached_original_edges = self._original_polydata.extract_all_edges() + return self._cached_original_edges + + def compute_distance(point1, point2) -> float: """Compute the distance between two points. @@ -322,17 +395,21 @@ def get_face_polydata( part = self._model.get_part(part_id) vertices, faces = self._get_vertices_and_surf_faces(face_facet_res, index) - surf = pv.PolyData(vertices, faces) - # Extract original edges before triangulation to preserve polygon boundaries - original_edges = surf.extract_all_edges() - # Triangulate to improve rendering of non-planar polygons - surf = surf.triangulate() - # Store original edges for rendering - surf._original_edges = original_edges + + # Create original polydata and store it for edge extraction + original_polydata = pv.PolyData(vertices, faces) + + # Create triangulated mesh for improved rendering of non-planar polygons + triangulated = original_polydata.triangulate() + + # Wrap in DisplayPolyData for clean access to original edges + display_mesh = DisplayPolyData(mesh=triangulated, original_mesh=original_polydata) + + # Set colors on the triangulated mesh fcolor = np.array(self.get_face_color(part, ColorByType.ZONE)) - colors = np.tile(fcolor, (surf.n_faces_strict, 1)) - surf["colors"] = colors - surf._disp_mesh = self + colors = np.tile(fcolor, (display_mesh.mesh.n_faces_strict, 1)) + display_mesh.mesh["colors"] = colors + display_mesh.mesh._disp_mesh = self has_mesh = True if face_facet_res.topo_face_ids[index] > 0: display_mesh_type = DisplayMeshType.TOPOFACE @@ -342,8 +419,8 @@ def get_face_polydata( display_mesh_type = DisplayMeshType.FACEZONELET id = face_facet_res.face_zonelet_ids[index] - if surf.n_points > 0: - return MeshObjectPlot(part, surf), DisplayMeshInfo( + if display_mesh.mesh.n_points > 0: + return MeshObjectPlot(part, display_mesh), DisplayMeshInfo( id=id, part_id=part_id, zone_id=face_facet_res.face_zone_ids[index], From cf0719578359de26ae2a66d2a712df1f9c48ea79 Mon Sep 17 00:00:00 2001 From: Martin Walters <104021577+waltersma@users.noreply.github.com> Date: Fri, 30 Jan 2026 13:38:37 +0000 Subject: [PATCH 05/11] Update plotter.py --- src/ansys/meshing/prime/graphics/plotter.py | 42 ++++++++++++++++----- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/ansys/meshing/prime/graphics/plotter.py b/src/ansys/meshing/prime/graphics/plotter.py index 1eddcffd55..61213b28d7 100644 --- a/src/ansys/meshing/prime/graphics/plotter.py +++ b/src/ansys/meshing/prime/graphics/plotter.py @@ -52,6 +52,15 @@ ] ) +# Polygon offset parameters for resolving z-fighting between faces and edge lines. +# These values control how much polygons are pushed back in the depth buffer: +# - FACTOR: Scales the maximum depth slope of the polygon (handles angled surfaces) +# - UNITS: Adds a constant depth offset (handles co-planar geometry) +# Values of 1.0 provide a good balance for most meshes without causing visual artifacts. +# Increase if z-fighting persists; decrease if faces appear to "pop" behind edges. +POLYGON_OFFSET_FACTOR = 1.0 +POLYGON_OFFSET_UNITS = 1.0 + class ColorByType(enum.IntEnum): """Contains the zone types to display.""" @@ -174,6 +183,8 @@ def add_model_pd(self, model_pd: Dict) -> None: model : Model Model to add to the plotter. """ + from ansys.meshing.prime.core.mesh import DisplayPolyData + for part_id, part_polydata in model_pd.items(): # proceed if scope won't be used or if the part is in the scope if "faces" in part_polydata.keys(): @@ -184,26 +195,34 @@ def add_model_pd(self, model_pd: Dict) -> None: colors = self.get_scalar_colors(face_mesh_info) has_mesh = face_mesh_info.has_mesh - if self._improved_surface_rendering: + # Check if mesh is wrapped in DisplayPolyData for improved rendering + mesh_obj = face_mesh_part.mesh + is_display_polydata = isinstance(mesh_obj, DisplayPolyData) + + if self._improved_surface_rendering and is_display_polydata: + # Get the triangulated mesh for rendering + mesh_to_render = mesh_obj.mesh + # Render subdivided faces without edges (edges shown separately) actor = self._backend.pv_interface.scene.add_mesh( - face_mesh_part.mesh, show_edges=False, color=colors, pickable=True + mesh_to_render, show_edges=False, color=colors, pickable=True ) # Apply polygon offset to push faces back in depth buffer # This prevents z-fighting with edge lines actor.GetProperty().SetEdgeVisibility(False) actor.GetMapper().SetResolveCoincidentTopologyToPolygonOffset() actor.GetMapper().SetRelativeCoincidentTopologyPolygonOffsetParameters( - 1.0, 1.0 + POLYGON_OFFSET_FACTOR, POLYGON_OFFSET_UNITS ) face_mesh_part.actor = actor self._backend.pv_interface._object_to_actors_map[actor] = face_mesh_part self._info_actor_map[actor] = face_mesh_info - # Render original polygon edges if mesh has edges and original edges exist - if has_mesh and hasattr(face_mesh_part.mesh, '_original_edges'): - original_edges = face_mesh_part.mesh._original_edges - if original_edges.n_points > 0: + # Render original polygon edges if available + # Edges are lazily extracted only when this code path is reached + if has_mesh and mesh_obj.has_original_edges(): + original_edges = mesh_obj.get_original_edges() + if original_edges is not None and original_edges.n_points > 0: self._backend.pv_interface.scene.add_mesh( original_edges, color='black', @@ -211,9 +230,14 @@ def add_model_pd(self, model_pd: Dict) -> None: pickable=False, ) else: - # Original rendering approach without triangulation + # Original rendering approach without improved surface rendering + # Use the original (non-triangulated) polydata if available + if is_display_polydata: + mesh_to_render = mesh_obj.get_original_polydata() + else: + mesh_to_render = mesh_obj actor = self._backend.pv_interface.scene.add_mesh( - face_mesh_part.mesh, show_edges=has_mesh, color=colors, pickable=True + mesh_to_render, show_edges=has_mesh, color=colors, pickable=True ) face_mesh_part.actor = actor self._backend.pv_interface._object_to_actors_map[actor] = face_mesh_part From 26bab6a7f8c787cefd909396359e878a9ddf5535 Mon Sep 17 00:00:00 2001 From: Martin Walters <104021577+waltersma@users.noreply.github.com> Date: Fri, 30 Jan 2026 13:46:04 +0000 Subject: [PATCH 06/11] Update src/ansys/meshing/prime/core/mesh.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ansys/meshing/prime/core/mesh.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ansys/meshing/prime/core/mesh.py b/src/ansys/meshing/prime/core/mesh.py index c2284cff4c..e18108ed65 100644 --- a/src/ansys/meshing/prime/core/mesh.py +++ b/src/ansys/meshing/prime/core/mesh.py @@ -130,14 +130,17 @@ class DisplayPolyData: ---------- mesh : pv.PolyData The triangulated mesh for rendering. - original_mesh : pv.PolyData, optional + original_polydata : pv.PolyData, optional The original (non-triangulated) mesh for edge extraction. """ - def __init__(self, mesh: pv.PolyData, original_mesh: pv.PolyData = None): + def __init__(self, mesh: pv.PolyData, original_polydata: pv.PolyData = None, **kwargs): """Initialize the display polydata.""" self._mesh = mesh - self._original_polydata = original_mesh + # Backward compatibility: accept legacy 'original_mesh' keyword. + if original_polydata is None and "original_mesh" in kwargs: + original_polydata = kwargs["original_mesh"] + self._original_polydata = original_polydata self._cached_original_edges = None @property From 3e91de23572d0ae270da8c136cf437431d937e62 Mon Sep 17 00:00:00 2001 From: Martin Walters <104021577+waltersma@users.noreply.github.com> Date: Fri, 30 Jan 2026 13:46:26 +0000 Subject: [PATCH 07/11] Update src/ansys/meshing/prime/graphics/plotter.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ansys/meshing/prime/graphics/plotter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/meshing/prime/graphics/plotter.py b/src/ansys/meshing/prime/graphics/plotter.py index 61213b28d7..e1029ab85a 100644 --- a/src/ansys/meshing/prime/graphics/plotter.py +++ b/src/ansys/meshing/prime/graphics/plotter.py @@ -225,7 +225,7 @@ def add_model_pd(self, model_pd: Dict) -> None: if original_edges is not None and original_edges.n_points > 0: self._backend.pv_interface.scene.add_mesh( original_edges, - color='black', + color="black", line_width=1, pickable=False, ) From 328ba17ad936c7e88f1995e33ad12b729fe3cd28 Mon Sep 17 00:00:00 2001 From: Martin Walters <104021577+waltersma@users.noreply.github.com> Date: Fri, 30 Jan 2026 13:50:04 +0000 Subject: [PATCH 08/11] Update src/ansys/meshing/prime/core/mesh.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ansys/meshing/prime/core/mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/meshing/prime/core/mesh.py b/src/ansys/meshing/prime/core/mesh.py index e18108ed65..bc387f1030 100644 --- a/src/ansys/meshing/prime/core/mesh.py +++ b/src/ansys/meshing/prime/core/mesh.py @@ -406,7 +406,7 @@ def get_face_polydata( triangulated = original_polydata.triangulate() # Wrap in DisplayPolyData for clean access to original edges - display_mesh = DisplayPolyData(mesh=triangulated, original_mesh=original_polydata) + display_mesh = DisplayPolyData(mesh=triangulated, original_polydata=original_polydata) # Set colors on the triangulated mesh fcolor = np.array(self.get_face_color(part, ColorByType.ZONE)) From 051d3bcbf4425d96d9c0379607f08239fdbfb6c7 Mon Sep 17 00:00:00 2001 From: Martin Walters <104021577+waltersma@users.noreply.github.com> Date: Fri, 30 Jan 2026 13:56:45 +0000 Subject: [PATCH 09/11] Update src/ansys/meshing/prime/graphics/plotter.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ansys/meshing/prime/graphics/plotter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ansys/meshing/prime/graphics/plotter.py b/src/ansys/meshing/prime/graphics/plotter.py index e1029ab85a..09e62077d1 100644 --- a/src/ansys/meshing/prime/graphics/plotter.py +++ b/src/ansys/meshing/prime/graphics/plotter.py @@ -209,7 +209,6 @@ def add_model_pd(self, model_pd: Dict) -> None: ) # Apply polygon offset to push faces back in depth buffer # This prevents z-fighting with edge lines - actor.GetProperty().SetEdgeVisibility(False) actor.GetMapper().SetResolveCoincidentTopologyToPolygonOffset() actor.GetMapper().SetRelativeCoincidentTopologyPolygonOffsetParameters( POLYGON_OFFSET_FACTOR, POLYGON_OFFSET_UNITS From 86d2f5ccbd8f45352a64ac89b3a19af008e388f6 Mon Sep 17 00:00:00 2001 From: Martin Walters <104021577+waltersma@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:08:49 +0000 Subject: [PATCH 10/11] Update mesh.py --- src/ansys/meshing/prime/core/mesh.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ansys/meshing/prime/core/mesh.py b/src/ansys/meshing/prime/core/mesh.py index bc387f1030..dafc000ca3 100644 --- a/src/ansys/meshing/prime/core/mesh.py +++ b/src/ansys/meshing/prime/core/mesh.py @@ -137,9 +137,6 @@ class DisplayPolyData: def __init__(self, mesh: pv.PolyData, original_polydata: pv.PolyData = None, **kwargs): """Initialize the display polydata.""" self._mesh = mesh - # Backward compatibility: accept legacy 'original_mesh' keyword. - if original_polydata is None and "original_mesh" in kwargs: - original_polydata = kwargs["original_mesh"] self._original_polydata = original_polydata self._cached_original_edges = None From c950b083b94b929b868559b1cdc8503bb6aa38ff Mon Sep 17 00:00:00 2001 From: Martin Walters <104021577+waltersma@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:11:52 +0000 Subject: [PATCH 11/11] Update mesh.py --- src/ansys/meshing/prime/core/mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/meshing/prime/core/mesh.py b/src/ansys/meshing/prime/core/mesh.py index dafc000ca3..c8f4ff0a54 100644 --- a/src/ansys/meshing/prime/core/mesh.py +++ b/src/ansys/meshing/prime/core/mesh.py @@ -134,7 +134,7 @@ class DisplayPolyData: The original (non-triangulated) mesh for edge extraction. """ - def __init__(self, mesh: pv.PolyData, original_polydata: pv.PolyData = None, **kwargs): + def __init__(self, mesh: pv.PolyData, original_polydata: pv.PolyData = None): """Initialize the display polydata.""" self._mesh = mesh self._original_polydata = original_polydata