Skip to content

Commit

Permalink
Add other views to Camera.tight (#3391)
Browse files Browse the repository at this point in the history
* refactor view_xyz, allow reuse of calculating view_vectors

* add multiple views to tight

* Typo fix

Co-authored-by: Andras Deak <adeak@users.noreply.github.com>

* fix naming to fix high variance image regression filtering

* Apply suggestions from code review

Co-authored-by: Andras Deak <adeak@users.noreply.github.com>

* fix return types

* better description of view planes

* fix negative issue for view_*

* move view_vectors in helpers and fix circular import

* use objects center as focal point, and offset by 1

* simplify absolute and dot products

* move test_tight_direction to be near other tight_* tests

* test for multiple objects when using tight

* Apply suggestions from code review: Comment clarifications

Co-authored-by: Andras Deak <adeak@users.noreply.github.com>

* Use parallel projection in docstring in tight

* add tests specifically for view_vectors

* don't use names in returns section

* Update pyvista/plotting/helpers.py

Co-authored-by: Andras Deak <adeak@users.noreply.github.com>
Co-authored-by: Alex Kaszynski <akascap@gmail.com>
  • Loading branch information
3 people committed Oct 11, 2022
1 parent f9dd706 commit 8683da1
Show file tree
Hide file tree
Showing 31 changed files with 189 additions and 56 deletions.
53 changes: 42 additions & 11 deletions pyvista/plotting/camera.py
Expand Up @@ -8,6 +8,8 @@
from pyvista import _vtk
from pyvista.utilities.misc import PyVistaDeprecationWarning

from .helpers import view_vectors


class Camera(_vtk.vtkCamera):
"""PyVista wrapper for the VTK Camera class.
Expand Down Expand Up @@ -645,9 +647,18 @@ def copy(self):

return new_camera

def tight(self, padding=0.0, adjust_render_window=True):
def tight(self, padding=0.0, adjust_render_window=True, view='xy', negative=False):
"""Adjust the camera position so that the actors fill the entire renderer.
The camera view direction is reoriented to be normal to the ``view``
plane. When ``negative=False``, The first letter of ``view`` refers
to the axis that points to the right. The second letter of ``view``
refers to axis that points up. When ``negative=True``, the first
letter refers to the axis that points left. The up direction is
unchanged.
Parallel projection is enabled when using this function.
Parameters
----------
padding : float, optional
Expand All @@ -658,9 +669,15 @@ def tight(self, padding=0.0, adjust_render_window=True):
Adjust the size of the render window as to match the dimensions of
the visible actors.
view : {'xy', 'yx', 'xz', 'zx', 'yz', 'zy'}
Plane to which the view is oriented. Default 'xy'.
negative : bool
Whether to view in opposite direction. Default ``False``.
Notes
-----
This resets the view direction to look at the XY plane.
This resets the view direction to look at a plane with parallel projection.
Examples
--------
Expand Down Expand Up @@ -690,23 +707,37 @@ def tight(self, padding=0.0, adjust_render_window=True):

self._renderer.ComputeAspect()
aspect = self._renderer.GetAspect()
angle = np.pi * self.view_angle / 180.0
dx, dy = (x1 - x0), (y1 - y0)
dist = max(dx / aspect[0], dy) / np.sin(angle / 2) / 2

self.SetViewUp(0, 1, 0)
self.SetPosition(x0 + dx / 2, y0 + dy / 2, dist * (1 + padding))
self.SetFocalPoint(x0 + dx / 2, y0 + dy / 2, 0)
position0 = np.array([x0, y0, z0])
position1 = np.array([x1, y1, z1])
objects_size = position1 - position0
position = position0 + objects_size / 2

direction, viewup = view_vectors(view, negative)
horizontal = np.cross(direction, viewup)

vert_dist = abs(objects_size @ viewup)
horiz_dist = abs(objects_size @ horizontal)

# set focal point to objects' center
# offset camera position from objects center by dist in opposite of viewing direction
# (actual distance doesn't matter due to parallel projection)
dist = 1
camera_position = position + dist * direction

self.SetViewUp(*viewup)
self.SetPosition(*camera_position)
self.SetFocalPoint(*position)

ps = max(dx / aspect[0], dy) / 2
ps = max(horiz_dist / aspect[0], vert_dist) / 2
self.parallel_scale = ps * (1 + padding)
self._renderer.ResetCameraClippingRange(x0, x1, y0, y1, z0, z1)

if adjust_render_window:
ren_win = self._renderer.GetRenderWindow()
size = list(ren_win.GetSize())
size_ratio = size[0] / size[1]
tight_ratio = dx / dy
tight_ratio = horiz_dist / vert_dist
resize_ratio = tight_ratio / size_ratio
if resize_ratio < 1:
size[0] = round(size[0] * resize_ratio)
Expand All @@ -717,4 +748,4 @@ def tight(self, padding=0.0, adjust_render_window=True):

# simply call tight again to reset the parallel scale due to the
# resized window
self.tight(padding=padding, adjust_render_window=False)
self.tight(padding=padding, adjust_render_window=False, view=view, negative=negative)
55 changes: 52 additions & 3 deletions pyvista/plotting/helpers.py
@@ -1,12 +1,12 @@
"""This module contains some convenience helper functions."""

from typing import Tuple

import numpy as np

import pyvista
from pyvista.utilities import is_pyvista_dataset

from .plotting import Plotter


def plot(
var_item,
Expand Down Expand Up @@ -227,7 +227,7 @@ def plot(
show_grid = kwargs.pop('show_grid', False)
auto_close = kwargs.get('auto_close')

pl = Plotter(
pl = pyvista.Plotter(
window_size=window_size,
off_screen=off_screen,
notebook=notebook,
Expand Down Expand Up @@ -462,3 +462,52 @@ def plot_itk(mesh, color=None, scalars=None, opacity=1.0, smooth_shading=False):
else:
pl.add_mesh(mesh, color, scalars, opacity, smooth_shading)
return pl.show()


def view_vectors(view: str, negative: bool = False) -> Tuple[np.ndarray, np.ndarray]:
"""Given a plane to view, return vectors for setting up camera.
Parameters
----------
view : {'xy', 'yx', 'xz', 'zx', 'yz', 'zy'}
Plane to return vectors for.
negative : bool
Whether to view from opposite direction. Default ``False``.
Returns
-------
vec : numpy.ndarray
``[x, y, z]`` vector that points in the viewing direction
viewup : numpy.ndarray
``[x, y, z]`` vector that points to the viewup direction
"""
if view == 'xy':
vec = np.array([0, 0, 1])
viewup = np.array([0, 1, 0])
elif view == 'yx':
vec = np.array([0, 0, -1])
viewup = np.array([1, 0, 0])
elif view == 'xz':
vec = np.array([0, -1, 0])
viewup = np.array([0, 0, 1])
elif view == 'zx':
vec = np.array([0, 1, 0])
viewup = np.array([1, 0, 0])
elif view == 'yz':
vec = np.array([1, 0, 0])
viewup = np.array([0, 0, 1])
elif view == 'zy':
vec = np.array([-1, 0, 0])
viewup = np.array([0, 1, 0])
else:
raise ValueError(
f"Unexpected value for direction {view}\n"
" Expected: 'xy', 'yx', 'xz', 'zx', 'yz', 'zy'"
)

if negative:
vec *= -1
return vec, viewup
37 changes: 7 additions & 30 deletions pyvista/plotting/renderer.py
Expand Up @@ -17,6 +17,7 @@
from .camera import Camera
from .charts import Charts
from .colors import Color
from .helpers import view_vectors
from .render_passes import RenderPasses
from .tools import create_axes_marker, create_axes_orientation_box, parse_font_family

Expand Down Expand Up @@ -2385,11 +2386,7 @@ def view_xy(self, negative=False):
>>> pl.show()
"""
vec = np.array([0, 0, 1])
viewup = np.array([0, 1, 0])
if negative:
vec *= -1
self.view_vector(vec, viewup)
self.view_vector(*view_vectors('xy', negative=negative))

def view_yx(self, negative=False):
"""View the YX plane.
Expand All @@ -2412,11 +2409,7 @@ def view_yx(self, negative=False):
>>> pl.show()
"""
vec = np.array([0, 0, -1])
viewup = np.array([1, 0, 0])
if negative:
vec *= -1
self.view_vector(vec, viewup)
self.view_vector(*view_vectors('yx', negative=negative))

def view_xz(self, negative=False):
"""View the XZ plane.
Expand All @@ -2439,11 +2432,7 @@ def view_xz(self, negative=False):
>>> pl.show()
"""
vec = np.array([0, -1, 0])
viewup = np.array([0, 0, 1])
if negative:
vec *= -1
self.view_vector(vec, viewup)
self.view_vector(*view_vectors('xz', negative=negative))

def view_zx(self, negative=False):
"""View the ZX plane.
Expand All @@ -2466,11 +2455,7 @@ def view_zx(self, negative=False):
>>> pl.show()
"""
vec = np.array([0, 1, 0])
viewup = np.array([1, 0, 0])
if negative:
vec *= -1
self.view_vector(vec, viewup)
self.view_vector(*view_vectors('zx', negative=negative))

def view_yz(self, negative=False):
"""View the YZ plane.
Expand All @@ -2493,11 +2478,7 @@ def view_yz(self, negative=False):
>>> pl.show()
"""
vec = np.array([1, 0, 0])
viewup = np.array([0, 0, 1])
if negative:
vec *= -1
self.view_vector(vec, viewup)
self.view_vector(*view_vectors('yz', negative=negative))

def view_zy(self, negative=False):
"""View the ZY plane.
Expand All @@ -2520,11 +2501,7 @@ def view_zy(self, negative=False):
>>> pl.show()
"""
vec = np.array([-1, 0, 0])
viewup = np.array([0, 1, 0])
if negative:
vec *= -1
self.view_vector(vec, viewup)
self.view_vector(*view_vectors('zy', negative=negative))

def disable(self):
"""Disable this renderer's camera from being interactive."""
Expand Down
7 changes: 7 additions & 0 deletions tests/plotting/conftest.py
Expand Up @@ -41,3 +41,10 @@ def check_gc(request):
assert len(after) == 0, 'Not all objects GCed:\n' + '\n'.join(
sorted(o.__class__.__name__ for o in after)
)


@pytest.fixture()
def colorful_tetrahedron():
mesh = pyvista.Tetrahedron()
mesh.cell_data["colors"] = [[255, 255, 255], [255, 0, 0], [0, 255, 0], [0, 0, 255]]
return mesh
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.
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.
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.
Binary file added tests/plotting/image_cache/view_xyz[False-xy].png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/plotting/image_cache/view_xyz[False-xz].png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/plotting/image_cache/view_xyz[False-yx].png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/plotting/image_cache/view_xyz[False-yz].png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/plotting/image_cache/view_xyz[False-zx].png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/plotting/image_cache/view_xyz[False-zy].png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/plotting/image_cache/view_xyz[True-xy].png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/plotting/image_cache/view_xyz[True-xz].png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/plotting/image_cache/view_xyz[True-yx].png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/plotting/image_cache/view_xyz[True-yz].png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/plotting/image_cache/view_xyz[True-zx].png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/plotting/image_cache/view_xyz[True-zy].png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions tests/plotting/test_helpers.py
@@ -0,0 +1,20 @@
"""Tests for plotting helpers."""

import numpy as np
import pytest

from pyvista.plotting.helpers import view_vectors


def test_view_vectors():
views = ('xy', 'yx', 'xz', 'zx', 'yz', 'zy')

for view in views:
vec, viewup = view_vectors(view)
assert isinstance(vec, np.ndarray)
assert np.array_equal(vec.shape, (3,))
assert isinstance(viewup, np.ndarray)
assert np.array_equal(viewup.shape, (3,))

with pytest.raises(ValueError, match="Unexpected value for direction"):
view_vectors('invalid')

0 comments on commit 8683da1

Please sign in to comment.