Repository: pyvista/pyvista

Issue:
add_floor colors wrong
The `add_floor` method is coloring the floor mesh now. They used to be a single, solid color

See https://docs.pyvista.org/examples/02-plot/floors.html#sphx-glr-examples-02-plot-floors-py

![sphx_glr_floors_001](https://user-images.githubusercontent.com/22067021/111492251-21b31180-8702-11eb-982e-e234aac30cd8.png)


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pyvista/pyvista/pull/1255?src=pr&el=h1) Report
> Merging [#1255](https://codecov.io/gh/pyvista/pyvista/pull/1255?src=pr&el=desc) (47c631a) into [master](https://codecov.io/gh/pyvista/pyvista/commit/a8e60eb360201c6e23919beb3482396767797dda?el=desc) (a8e60eb) will **increase** coverage by `0.00%`.
> The diff coverage is `37.50%`.

```diff
@@           Coverage Diff           @@
##           master    #1255   +/-   ##
=======================================
  Coverage   88.90%   88.90%           
=======================================
  Files          55       55           
  Lines       11188    11189    +1     
=======================================
+ Hits         9947     9948    +1     
  Misses       1241     1241           
```


Patch:
diff --git a/docs/core/camera.rst b/docs/core/camera.rst
new file mode 100644
index 00000000..f4218df3
--- /dev/null
+++ b/docs/core/camera.rst
@@ -0,0 +1,79 @@
+Cameras
+=======
+The :class:`pyvista.Camera` class adds additional functionality and a pythonic API
+to the ``vtk.vtkCamera`` class. :class:`pyvista.Camera` objects come with a default
+set of cameras that work well in most cases, but in many situations a more hands-on
+access to camera is necessary.
+
+
+Brief Example
+-------------
+
+Create a frustum of camera, then create a scene of inside frustum.
+
+.. jupyter-execute::
+
+    import pyvista as pv
+    import numpy as np
+    import vtk
+    from pyvista import examples
+
+    pv.set_plot_theme("document")
+
+    camera = pv.Camera()
+    near_range = 0.3
+    far_range = 0.8
+    camera.clipping_range = (near_range, far_range)
+    unit_vector = np.array(camera.direction) / np.linalg.norm(
+        np.array([camera.focal_point]) - np.array([camera.position])
+    )
+
+    frustum = camera.view_frustum(1.0)
+
+    position = camera.position
+    focal_point = camera.focal_point
+    line = pv.Line(position, focal_point)
+
+    bunny = examples.download_bunny()
+    xyz = camera.position + unit_vector * 0.6 - np.mean(bunny.points, axis=0)
+    bunny.translate(xyz)
+
+    p = pv.Plotter(shape=(2, 1))
+    p.subplot(0, 0)
+    p.add_text("Camera Position")
+    p.add_mesh(bunny)
+    p.add_mesh(frustum, style="wireframe")
+    p.add_mesh(bunny)
+    p.add_mesh(line, color="b")
+    p.add_point_labels(
+        [
+            position,
+            camera.position + unit_vector * near_range,
+            camera.position + unit_vector * far_range,
+            focal_point,
+        ],
+        ["Camera Position", "Near Clipping Plane", "Far Clipping Plane", "Focal Point"],
+        margin=0,
+        fill_shape=False,
+        font_size=14,
+        shape_color="white",
+        point_color="red",
+        text_color="black",
+    )
+    p.camera.position = (1.1, 1.5, 0.0)
+    p.camera.focal_point = (0.2, 0.3, 0.3)
+    p.camera.up = (0.0, 1.0, 0.0)
+    p.camera.zoom(1.4)
+
+    p.subplot(1, 0)
+    p.add_text("Camera View")
+    p.add_mesh(bunny)
+    p.camera = camera
+    p.show()
+
+
+API reference
+~~~~~~~~~~~~~
+
+.. autoclass:: pyvista.Camera
+   :members:
diff --git a/docs/core/index.rst b/docs/core/index.rst
index 776b39af..837094be 100644
--- a/docs/core/index.rst
+++ b/docs/core/index.rst
@@ -40,5 +40,6 @@ PyVista has the following mesh types:
    grids
    composite
    filters
+   camera
    lights
    helpers
diff --git a/pyvista/_vtk.py b/pyvista/_vtk.py
index 6e0c1e5e..58abd08b 100644
--- a/pyvista/_vtk.py
+++ b/pyvista/_vtk.py
@@ -304,7 +304,8 @@ if VTK9:
                                               vtkRegularPolygonSource,
                                               vtkLineSource,
                                               vtkPointSource,
-                                              vtkArrowSource)
+                                              vtkArrowSource,
+                                              vtkFrustumSource)
     from vtkmodules.vtkFiltersGeometry import (vtkGeometryFilter,
                                                vtkStructuredGridGeometryFilter,
                                                vtkCompositeDataGeometryFilter,
diff --git a/pyvista/plotting/camera.py b/pyvista/plotting/camera.py
index 0219dc77..e8e0bdcb 100644
--- a/pyvista/plotting/camera.py
+++ b/pyvista/plotting/camera.py
@@ -2,6 +2,7 @@
 
 import numpy as np
 
+import pyvista
 from pyvista import _vtk
 
 
@@ -50,7 +51,7 @@ class Camera(_vtk.vtkCamera):
         --------
         >>> import pyvista
         >>> pl = pyvista.Plotter()
-        >>> pl.camera.position = 2.0, 1.0, 1.0
+        >>> pl.camera.position = (2.0, 1.0, 1.0)
         """
         self.SetPosition(value)
 
@@ -245,7 +246,7 @@ class Camera(_vtk.vtkCamera):
 
     @property
     def clipping_range(self):
-        """Return the Clipping range.
+        """Return the location of the near and far clipping planes along the direction of projection.
 
         Examples
         --------
@@ -258,7 +259,7 @@ class Camera(_vtk.vtkCamera):
 
     @clipping_range.setter
     def clipping_range(self, points):
-        """Set the clipping range.
+        """Set the location of the near and far clipping planes along the direction of projection.
 
         Examples
         --------
@@ -302,3 +303,40 @@ class Camera(_vtk.vtkCamera):
 
         """
         return self.GetDirectionOfProjection()
+
+    def view_frustum(self, aspect=1.0):
+        """Get the view frustum.
+
+        Parameters
+        ----------
+        aspect : float, optional
+            The aspect of the viewport to compute the planes. Default to 1.0.
+
+        Returns
+        -------
+        frustum : pv.PolyData
+            View frustum.
+
+        Examples
+        --------
+        >>> import pyvista
+        >>> plotter = pyvista.Plotter()
+        >>> frustum = plotter.camera.view_frustum(1.0)
+        >>> frustum.n_points
+        8
+        >>> frustum.n_cells
+        6
+
+        """
+        frustum_planes = [0] * 24
+        self.GetFrustumPlanes(aspect, frustum_planes)
+        planes = _vtk.vtkPlanes()
+        planes.SetFrustumPlanes(frustum_planes)
+
+        frustum_source = _vtk.vtkFrustumSource()
+        frustum_source.ShowLinesOff()
+        frustum_source.SetPlanes(planes)
+        frustum_source.Update()
+
+        frustum = pyvista.wrap(frustum_source.GetOutput())
+        return frustum
diff --git a/pyvista/plotting/renderer.py b/pyvista/plotting/renderer.py
index 0e27331b..2b05ae68 100644
--- a/pyvista/plotting/renderer.py
+++ b/pyvista/plotting/renderer.py
@@ -115,6 +115,7 @@ class Renderer(_vtk.vtkRenderer):
         self.bounding_box_actor = None
         self.scale = [1.0, 1.0, 1.0]
         self.AutomaticLightCreationOff()
+        self._floor = None
         self._floors = []
         self._floor_kwargs = []
         # this keeps track of lights added manually to prevent garbage collection
@@ -940,8 +941,8 @@ class Renderer(_vtk.vtkRenderer):
                   offset=0.0, pickable=False, store_floor_kwargs=True):
         """Show a floor mesh.
 
-        This generates planes at the boundaries of the scene to behave like
-        floors or walls.
+        This generates planes at the boundaries of the scene to behave
+        like floors or walls.
 
         Parameters
         ----------
@@ -963,29 +964,40 @@ class Renderer(_vtk.vtkRenderer):
             Either a string, rgb list, or hex color string.
 
         line_width : int
-            Thickness of the edges. Only if ``show_edges`` is ``True``
+            Thickness of the edges. Only if ``show_edges`` is ``True``.
 
         opacity : float
-            The opacity of the generated surface
+            The opacity of the generated surface.
 
         show_edges : bool
             Flag on whether to show the mesh edges for tiling.
 
         ine_width : float, optional
             Thickness of lines.  Only valid for wireframe and surface
-            representations.  Default None.
+            representations.  Default ``None``.
 
         lighting : bool, optional
-            Enable or disable view direction lighting.  Default False.
+            Enable or disable view direction lighting.  Default ``False``.
 
         edge_color : string or 3 item list, optional
             Color of of the edges of the mesh.
 
-        pad : float
-            Percentage padding between 0 and 1
+        pad : float, optional
+            Percentage padding between 0 and 1.
+
+        offset : float, optional
+            Percentage offset along plane normal.
+
+        Examples
+        --------
+        Add a floor below a sphere and plot it.
+
+        >>> import pyvista
+        >>> pl = pyvista.Plotter()
+        >>> _ = pl.add_mesh(pyvista.Sphere())
+        >>> _ = pl.add_floor()
+        >>> pl.show()  # doctest:+SKIP
 
-        offset : float
-            Percentage offset along plane normal
         """
         if store_floor_kwargs:
             kwargs = locals()
@@ -996,32 +1008,32 @@ class Renderer(_vtk.vtkRenderer):
         center = np.array(self.center)
         if face.lower() in '-z':
             center[2] = self.bounds[4] - (ranges[2] * offset)
-            normal = (0,0,1)
+            normal = (0, 0, 1)
             i_size = ranges[0]
             j_size = ranges[1]
         elif face.lower() in '-y':
             center[1] = self.bounds[2] - (ranges[1] * offset)
-            normal = (0,1,0)
+            normal = (0, 1, 0)
             i_size = ranges[0]
             j_size = ranges[2]
         elif face.lower() in '-x':
             center[0] = self.bounds[0] - (ranges[0] * offset)
-            normal = (1,0,0)
+            normal = (1, 0, 0)
             i_size = ranges[2]
             j_size = ranges[1]
         elif face.lower() in '+z':
             center[2] = self.bounds[5] + (ranges[2] * offset)
-            normal = (0,0,-1)
+            normal = (0, 0, -1)
             i_size = ranges[0]
             j_size = ranges[1]
         elif face.lower() in '+y':
             center[1] = self.bounds[3] + (ranges[1] * offset)
-            normal = (0,-1,0)
+            normal = (0, -1, 0)
             i_size = ranges[0]
             j_size = ranges[2]
         elif face.lower() in '+x':
             center[0] = self.bounds[1] + (ranges[0] * offset)
-            normal = (-1,0,0)
+            normal = (-1, 0, 0)
             i_size = ranges[2]
             j_size = ranges[1]
         else:
@@ -1030,8 +1042,8 @@ class Renderer(_vtk.vtkRenderer):
                                     i_size=i_size, j_size=j_size,
                                     i_resolution=i_resolution,
                                     j_resolution=j_resolution)
-        name = f'Floor({face})'
-        # use floor
+        self._floor.clear_arrays()
+
         if lighting is None:
             lighting = rcParams['lighting']
 
@@ -1046,7 +1058,7 @@ class Renderer(_vtk.vtkRenderer):
         mapper.SetInputData(self._floor)
         actor, prop = self.add_actor(mapper,
                                      reset_camera=reset_camera,
-                                     name=name, pickable=pickable)
+                                     name=f'Floor({face})', pickable=pickable)
 
         prop.SetColor(rgb_color)
         prop.SetOpacity(opacity)


Test patch:
diff --git a/tests/test_camera.py b/tests/test_camera.py
index b9e82464..2d299b10 100644
--- a/tests/test_camera.py
+++ b/tests/test_camera.py
@@ -97,3 +97,9 @@ def test_view_angle(camera):
 
 def test_direction(camera):
     assert camera.GetDirectionOfProjection() == camera.direction
+
+
+def test_view_frustum(camera):
+    frustum = camera.view_frustum(1.0)
+    assert frustum.n_points == 8
+    assert frustum.n_cells == 6