Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix active normals for properties point_normals, cell_normals, and method plot_normals with smooth_shading #6062

Merged
merged 10 commits into from
May 13, 2024
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