Skip to content

Commit

Permalink
Fix active normals for properties point_normals, cell_normals, an…
Browse files Browse the repository at this point in the history
…d method `plot_normals` with `smooth_shading` (#6062)

* Fix point_normals and cell_normals properties

* Add plot_normals test

* Add fix for smooth shading

* Update test

* Update docs

* Apply suggestions from code review

Co-authored-by: Tetsuo Koyama <tkoyama010@gmail.com>

* Update backface param test image

---------

Co-authored-by: Tetsuo Koyama <tkoyama010@gmail.com>
  • Loading branch information
user27182 and tkoyama010 committed May 13, 2024
1 parent c4359ae commit 0b44c29
Show file tree
Hide file tree
Showing 9 changed files with 79 additions and 43 deletions.
22 changes: 10 additions & 12 deletions pyvista/core/pointset.py
Original file line number Diff line number Diff line change
Expand Up @@ -1496,10 +1496,9 @@ def volume(self) -> float: # numpydoc ignore=RT01
def point_normals(self) -> pyvista.pyvista_ndarray: # numpydoc ignore=RT01
"""Return the point normals.
If the point data already contains an array named ``'Normals'``, this
array will be returned. Otherwise, the normals will be computed using
the default options of :func:`compute_normals()
<pyvista.PolyDataFilters.compute_normals>` and returned.
The active point normals are returned if they exist. Otherwise, they
are computed with :func:`~pyvista.PolyDataFilters.compute_normals`
using the default options.
Returns
-------
Expand All @@ -1520,8 +1519,8 @@ def point_normals(self) -> pyvista.pyvista_ndarray: # numpydoc ignore=RT01
[ 0.10575636, -0.02247921, -0.99413794]], dtype=float32)
"""
if 'Normals' in self.point_data:
normals = self.point_data['Normals']
if self.point_data.active_normals is not None:
normals = self.point_data.active_normals
else:
normals = self.compute_normals(cell_normals=False, inplace=False).point_data['Normals']
return normals
Expand All @@ -1530,10 +1529,9 @@ def point_normals(self) -> pyvista.pyvista_ndarray: # numpydoc ignore=RT01
def cell_normals(self) -> pyvista.pyvista_ndarray: # numpydoc ignore=RT01
"""Return the cell normals.
If the cell data already contains an array named ``'Normals'``, this
array will be returned. Otherwise, the normals will be computed using
the default options of :func:`compute_normals()
<pyvista.PolyDataFilters.compute_normals>` and returned.
The active cell normals are returned if they exist. Otherwise, they
are computed with :func:`~pyvista.PolyDataFilters.compute_normals`
using the default options.
Returns
-------
Expand All @@ -1554,8 +1552,8 @@ def cell_normals(self) -> pyvista.pyvista_ndarray: # numpydoc ignore=RT01
[ 0.16175848, -0.01700151, -0.9866839 ]], dtype=float32)
"""
if 'Normals' in self.cell_data:
normals = self.cell_data['Normals']
if self.cell_data.active_normals is not None:
normals = self.cell_data.active_normals
else:
normals = self.compute_normals(point_normals=False, inplace=False).cell_data['Normals']
return normals
Expand Down
4 changes: 1 addition & 3 deletions pyvista/plotting/_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,7 @@ def prepare_smooth_shading(mesh, scalars, texture, split_sharp_edges, feature_an
if has_scalars and use_points:
# we must track the original IDs with our own array from compute_normals
indices_array = 'pyvistaOriginalPointIds'
else:
# consider checking if mesh contains active normals
# if mesh.point_data.active_normals is None:
elif mesh.point_data.active_normals is None:
mesh.compute_normals(cell_normals=False, inplace=True)
except TypeError as e:
if "Normals cannot be computed" in repr(e):
Expand Down
83 changes: 55 additions & 28 deletions tests/core/test_polydata.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,34 +775,61 @@ def test_compute_normals_split_vertices(cube):
assert len(set(cube_split_norm.point_data['pyvistaOriginalPointIds'])) == 8


def test_point_normals(sphere):
sphere = sphere.compute_normals(cell_normals=False, point_normals=True)

# when `Normals` already exist, make sure they are returned
normals = sphere.point_normals
assert normals.shape[0] == sphere.n_points
assert np.all(normals == sphere.point_data['Normals'])
assert np.shares_memory(normals, sphere.point_data['Normals'])

# when they don't, compute them
sphere.point_data.pop('Normals')
normals = sphere.point_normals
assert normals.shape[0] == sphere.n_points


def test_cell_normals(sphere):
sphere = sphere.compute_normals(cell_normals=True, point_normals=False)

# when `Normals` already exist, make sure they are returned
normals = sphere.cell_normals
assert normals.shape[0] == sphere.n_cells
assert np.all(normals == sphere.cell_data['Normals'])
assert np.shares_memory(normals, sphere.cell_data['Normals'])

# when they don't, compute them
sphere.cell_data.pop('Normals')
normals = sphere.cell_normals
assert normals.shape[0] == sphere.n_cells
@pytest.fixture()
def ant_with_normals(ant):
ant['Scalars'] = range(ant.n_points)
point_normals = [[0, 0, 1]] * ant.n_points
ant.point_data['PointNormals'] = point_normals
ant.point_data.active_normals_name = 'PointNormals'

cell_normals = [[1, 0, 0]] * ant.n_cells
ant.cell_data['CellNormals'] = cell_normals
ant.cell_data.active_normals_name = 'CellNormals'
return ant


def test_point_normals_returns_active_normals(ant_with_normals):
ant = ant_with_normals
expected_point_normals = ant['PointNormals']

actual_point_normals = ant.point_normals
assert actual_point_normals.shape[0] == ant.n_points
assert np.array_equal(actual_point_normals, ant.point_data.active_normals)
assert np.shares_memory(actual_point_normals, ant.point_data.active_normals)
assert np.array_equal(actual_point_normals, expected_point_normals)


def test_point_normals_computes_new_normals(ant):
expected_point_normals = ant.copy().compute_normals().point_data['Normals']
ant.point_data.clear()
assert ant.array_names == []
assert ant.point_data.active_normals is None

actual_point_normals = ant.point_normals
assert actual_point_normals.shape[0] == ant.n_points
assert np.array_equal(actual_point_normals, expected_point_normals)


def test_cell_normals_returns_active_normals(ant_with_normals):
ant = ant_with_normals
expected_cell_normals = ant['CellNormals']

actual_cell_normals = ant.cell_normals
assert actual_cell_normals.shape[0] == ant.n_cells
assert np.array_equal(actual_cell_normals, ant.cell_data.active_normals)
assert np.shares_memory(actual_cell_normals, ant.cell_data.active_normals)
assert np.array_equal(actual_cell_normals, expected_cell_normals)


def test_cell_normals_computes_new_normals(ant):
expected_cell_normals = ant.copy().compute_normals().cell_data['Normals']
ant.cell_data.clear()
assert ant.array_names == []
assert ant.cell_data.active_normals is None

actual_cell_normals = ant.cell_normals
assert actual_cell_normals.shape[0] == ant.n_cells
assert np.array_equal(actual_cell_normals, expected_cell_normals)


def test_face_normals(sphere):
Expand Down
Binary file modified tests/plotting/image_cache/backface_params.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions tests/plotting/test_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -2644,6 +2644,19 @@ def test_splitting():
)


@pytest.mark.parametrize('smooth_shading', [True, False])
@pytest.mark.parametrize('use_custom_normals', [True, False])
def test_plot_normals_smooth_shading(sphere, use_custom_normals, smooth_shading):
sphere = pv.Sphere(phi_resolution=5, theta_resolution=5)
sphere.clear_data()

if use_custom_normals:
normals = [[0, 0, -1]] * sphere.n_points
sphere.point_data.active_normals = normals

sphere.plot_normals(show_mesh=True, color='red', smooth_shading=smooth_shading)


@skip_mac_flaky
def test_splitting_active_cells(cube):
cube.cell_data['cell_id'] = range(cube.n_cells)
Expand Down

0 comments on commit 0b44c29

Please sign in to comment.