Repository: pyvista/pyvista

Issue:
Clip entire scene
We should implement a feature to easily clip by a box all of the actors in a rendering scene, for example:

![308f50d2b9f7f1aa5d3bff619fd7704883c2976e_2_1326x1000](https://user-images.githubusercontent.com/22067021/55899715-b5791c00-5b82-11e9-916f-88f87b423fee.jpeg)


Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/pyvista/pyvista/pull/270?src=pr&el=h1) Report
> Merging [#270](https://codecov.io/gh/pyvista/pyvista/pull/270?src=pr&el=desc) into [master](https://codecov.io/gh/pyvista/pyvista/commit/9fef5971739dd939da62e88adb0ec8ca3eba62c6?src=pr&el=desc) will **decrease** coverage by `1.22%`.
> The diff coverage is `70.12%`.

[![Impacted file tree graph](https://codecov.io/gh/pyvista/pyvista/pull/270/graphs/tree.svg?width=650&token=KzeS9JHDaQ&height=150&src=pr)](https://codecov.io/gh/pyvista/pyvista/pull/270?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #270      +/-   ##
==========================================
- Coverage    83.5%   82.27%   -1.23%     
==========================================
  Files          23       23              
  Lines        6389     6527     +138     
==========================================
+ Hits         5335     5370      +35     
- Misses       1054     1157     +103
```


| [Impacted Files](https://codecov.io/gh/pyvista/pyvista/pull/270?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pyvista/plotting.py](https://codecov.io/gh/pyvista/pyvista/pull/270/diff?src=pr&el=tree#diff-cHl2aXN0YS9wbG90dGluZy5weQ==) | `76.57% <100%> (ø)` | :arrow_up: |
| [pyvista/filters.py](https://codecov.io/gh/pyvista/pyvista/pull/270/diff?src=pr&el=tree#diff-cHl2aXN0YS9maWx0ZXJzLnB5) | `88.56% <100%> (+0.04%)` | :arrow_up: |
| [pyvista/\_\_init\_\_.py](https://codecov.io/gh/pyvista/pyvista/pull/270/diff?src=pr&el=tree#diff-cHl2aXN0YS9fX2luaXRfXy5weQ==) | `89.18% <100%> (ø)` | :arrow_up: |
| [pyvista/composite.py](https://codecov.io/gh/pyvista/pyvista/pull/270/diff?src=pr&el=tree#diff-cHl2aXN0YS9jb21wb3NpdGUucHk=) | `69.41% <69.41%> (ø)` | |
| [pyvista/features.py](https://codecov.io/gh/pyvista/pyvista/pull/270/diff?src=pr&el=tree#diff-cHl2aXN0YS9mZWF0dXJlcy5weQ==) | `16% <0%> (-1.4%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pyvista/pyvista/pull/270?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pyvista/pyvista/pull/270?src=pr&el=footer). Last update [9fef597...9780421](https://codecov.io/gh/pyvista/pyvista/pull/270?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/types/composite.rst b/docs/types/composite.rst
new file mode 100644
index 00000000..7dc8c237
--- /dev/null
+++ b/docs/types/composite.rst
@@ -0,0 +1,39 @@
+Composite Datasets
+==================
+
+.. automodule:: pyvista.composite
+   :show-inheritance:
+
+MultiBlock Datasets
+-------------------
+
+.. rubric:: Attributes
+
+.. autoautosummary:: pyvista.MultiBlock
+   :attributes:
+
+.. rubric:: Methods
+
+.. autoautosummary:: pyvista.MultiBlock
+   :methods:
+
+
+.. autoclass:: pyvista.MultiBlock
+   :show-inheritance:
+   :members:
+   :undoc-members:
+
+
+
+Composite Filters
+-----------------
+
+.. rubric:: Methods
+
+.. autoautosummary:: pyvista.CompositeFilters
+   :methods:
+
+.. autoclass:: pyvista.CompositeFilters
+   :show-inheritance:
+   :members:
+   :undoc-members:
diff --git a/docs/types/container.rst b/docs/types/container.rst
deleted file mode 100644
index 86549d89..00000000
--- a/docs/types/container.rst
+++ /dev/null
@@ -1,21 +0,0 @@
-Multiple Block Datasets
-=======================
-
-.. automodule:: pyvista.container
-   :show-inheritance:
-
-.. rubric:: Attributes
-
-.. autoautosummary:: pyvista.MultiBlock
-   :attributes:
-
-.. rubric:: Methods
-
-.. autoautosummary:: pyvista.MultiBlock
-   :methods:
-
-
-.. autoclass:: pyvista.MultiBlock
-   :show-inheritance:
-   :members:
-   :undoc-members:
diff --git a/docs/types/index.rst b/docs/types/index.rst
index e179daa7..5336eff5 100644
--- a/docs/types/index.rst
+++ b/docs/types/index.rst
@@ -17,4 +17,4 @@ side to help new users understand which data types to use.
    points
    point-grids
    grids
-   container
+   composite
diff --git a/pyvista/__init__.py b/pyvista/__init__.py
index 51b9afc8..42b6ad86 100755
--- a/pyvista/__init__.py
+++ b/pyvista/__init__.py
@@ -6,7 +6,7 @@ from pyvista.errors import *
 from pyvista.readers import *
 from pyvista.colors import *
 from pyvista.features import *
-from pyvista.filters import DataSetFilters
+from pyvista.filters import DataSetFilters, CompositeFilters
 from pyvista.common import Common
 from pyvista.pointset import PointGrid
 from pyvista.pointset import PolyData
@@ -17,7 +17,7 @@ from pyvista.grid import RectilinearGrid
 from pyvista.grid import UniformGrid
 from pyvista.geometric_objects import *
 from pyvista.parametric_geometry import *
-from pyvista.container import MultiBlock
+from pyvista.composite import MultiBlock
 from pyvista.qt_plotting import QtInteractor
 from pyvista.qt_plotting import BackgroundPlotter
 from pyvista.export import export_plotter_vtkjs, get_vtkjs_url
diff --git a/pyvista/container.py b/pyvista/composite.py
similarity index 84%
rename from pyvista/container.py
rename to pyvista/composite.py
index 728bc5d2..238c2351 100644
--- a/pyvista/container.py
+++ b/pyvista/composite.py
@@ -14,17 +14,17 @@ from vtk import vtkMultiBlockDataSet
 import pyvista
 from pyvista import plot
 from pyvista.utilities import get_scalar, is_pyvista_obj, wrap
+from pyvista.filters import CompositeFilters
 
 log = logging.getLogger(__name__)
 log.setLevel('CRITICAL')
 
 
-
-class MultiBlock(vtkMultiBlockDataSet):
+class MultiBlock(vtkMultiBlockDataSet, CompositeFilters):
     """
-    A container class to hold many data sets which can be iterated over.
+    A composite class to hold many data sets which can be iterated over.
     This wraps/extends the ``vtkMultiBlockDataSet`` class in VTK so that we can
-    easily plot these data sets and use the container in a Pythonic manner.
+    easily plot these data sets and use the composite in a Pythonic manner.
     """
 
     # Bind pyvista.plotting.plot to the object
@@ -41,6 +41,7 @@ class MultiBlock(vtkMultiBlockDataSet):
                     self.DeepCopy(args[0])
                 else:
                     self.ShallowCopy(args[0])
+                self.wrap_nested()
             elif isinstance(args[0], (list, tuple)):
                 for block in args[0]:
                     self.append(block)
@@ -55,32 +56,15 @@ class MultiBlock(vtkMultiBlockDataSet):
             # keep a reference of the args
             self.refs.append(args)
 
-    def extract_geometry(self):
-        """Combines the geomertry of all blocks into a single ``PolyData``
-        object. Place this filter at the end of a pipeline before a polydata
-        consumer such as a polydata mapper to extract geometry from all blocks
-        and append them to one polydata object.
-        """
-        gf = vtk.vtkCompositeDataGeometryFilter()
-        gf.SetInputData(self)
-        gf.Update()
-        return wrap(gf.GetOutputDataObject(0))
-
-    def combine(self, merge_points=False):
-        """Appends all blocks into a single unstructured grid.
-
-        Parameters
-        ----------
-        merge_points : bool, optional
-            Merge coincidental points.
 
-        """
-        alg = vtk.vtkAppendFilter()
-        for block in self:
-            alg.AddInputData(block)
-        alg.SetMergePoints(merge_points)
-        alg.Update()
-        return wrap(alg.GetOutputDataObject(0))
+    def wrap_nested(self):
+        """A helper to ensure all nested data structures are wrapped as PyVista
+        datasets. This is perfrom inplace"""
+        for i in range(self.n_blocks):
+            block = self.GetBlock(i)
+            if not is_pyvista_obj(block):
+                self.SetBlock(i, pyvista.wrap(block))
+        return
 
 
     def _load_file(self, filename):
@@ -165,17 +149,27 @@ class MultiBlock(vtkMultiBlockDataSet):
 
         # get bounds for each block and update
         for i in range(self.n_blocks):
-            try:
-                bnds = self[i].GetBounds()
-                for a in range(3):
-                    bounds = update_bounds(a, bnds, bounds)
-            except AttributeError:
-                # Data object doesn't have bounds or is None
-                pass
+            if self[i] is None:
+                continue
+            bnds = self[i].bounds
+            for a in range(3):
+                bounds = update_bounds(a, bnds, bounds)
 
         return bounds
 
 
+    @property
+    def center(self):
+        """ Center of the bounding box """
+        return np.array(self.bounds).reshape(3,2).mean(axis=1)
+
+
+    @property
+    def length(self):
+        """the length of the diagonal of the bounding box"""
+        return pyvista.Box(self.bounds).length
+
+
     @property
     def n_blocks(self):
         """The total number of blocks set"""
@@ -189,6 +183,23 @@ class MultiBlock(vtkMultiBlockDataSet):
         self.Modified()
 
 
+    @property
+    def volume(self):
+        """
+        Total volume of all meshes in this dataast
+
+        Returns
+        -------
+        volume : float
+            Total volume of the mesh.
+
+        """
+        volume = 0.0
+        for block in self:
+            volume += block.volume
+        return volume
+
+
     def get_data_range(self, name):
         """Gets the min/max of a scalar given its name across all blocks"""
         mini, maxi = np.inf, -np.inf
@@ -341,20 +352,31 @@ class MultiBlock(vtkMultiBlockDataSet):
         return data
 
 
-    ## TODO: I can't get this to work as expected
-    # def clean(self):
-    #     """This will remove any null blocks"""
-    #     nvalid = 0
-    #     for i in range(self.n_blocks):
-    #         print(i, type(self[i]), self[i], self.get_block_name(i))
-    #         if self[i] is None:
-    #             print('removing', i)
-    #             del self[i]
-    #         else:
-    #             nvalid += 1
-    #     #self.n_blocks = nvalid
-    #     print('nvalid', nvalid)
-    #     return
+    def clean(self, empty=True):
+        """This will remove any null blocks in place
+        Parameters
+        -----------
+        empty : bool
+            Remove any meshes that are empty as well (have zero points)
+        """
+        null_blocks = []
+        for i in range(self.n_blocks):
+            if isinstance(self[i], MultiBlock):
+                # Recursively move through nested structures
+                self[i].clean()
+                if self[i].n_blocks < 1:
+                    null_blocks.append(i)
+            elif self[i] is None:
+                null_blocks.append(i)
+            elif empty and self[i].n_points < 1:
+                null_blocks.append(i)
+        # Now remove the null/empty meshes
+        null_blocks = np.array(null_blocks, dtype=int)
+        for i in range(len(null_blocks)):
+            # Cast as int because windows is super annoying
+            del self[int(null_blocks[i])]
+            null_blocks -= 1
+        return
 
 
     def _get_attrs(self):
diff --git a/pyvista/filters.py b/pyvista/filters.py
index b6b286d8..87d8df8a 100644
--- a/pyvista/filters.py
+++ b/pyvista/filters.py
@@ -47,9 +47,10 @@ def _get_output(algorithm, iport=0, iconnection=0, oport=0, active_scalar=None,
     """A helper to get the algorithm's output and copy input's pyvista meta info"""
     ido = algorithm.GetInputDataObject(iport, iconnection)
     data = wrap(algorithm.GetOutputDataObject(oport))
-    data.copy_meta_from(ido)
-    if active_scalar is not None:
-        data.set_active_scalar(active_scalar, preference=active_scalar_field)
+    if not isinstance(data, pyvista.MultiBlock):
+        data.copy_meta_from(ido)
+        if active_scalar is not None:
+            data.set_active_scalar(active_scalar, preference=active_scalar_field)
     return data
 
 
@@ -186,8 +187,6 @@ class DataSetFilters(object):
         # find center of data if origin not specified
         if origin is None:
             origin = dataset.center
-        if not is_inside_bounds(origin, dataset.bounds):
-            raise AssertionError('Slice is outside data bounds.')
         # create the plane for clipping
         plane = _generate_plane(normal, origin)
         # create slice
@@ -206,7 +205,7 @@ class DataSetFilters(object):
     def slice_orthogonal(dataset, x=None, y=None, z=None,
                          generate_triangles=False, contour=False):
         """Creates three orthogonal slices through the dataset on the three
-        caresian planes. Yields a MutliBlock dataset of the three slices
+        caresian planes. Yields a MutliBlock dataset of the three slices.
 
         Parameters
         ----------
@@ -227,7 +226,6 @@ class DataSetFilters(object):
             If True, apply a ``contour`` filter after slicing
 
         """
-        output = pyvista.MultiBlock()
         # Create the three slices
         if x is None:
             x = dataset.center[0]
@@ -235,6 +233,13 @@ class DataSetFilters(object):
             y = dataset.center[1]
         if z is None:
             z = dataset.center[2]
+        output = pyvista.MultiBlock()
+        if isinstance(dataset, pyvista.MultiBlock):
+            for i in range(dataset.n_blocks):
+                output[i] = dataset[i].slice_orthogonal(x=x, y=y, z=z,
+                    generate_triangles=generate_triangles,
+                    contour=contour)
+            return output
         output[0, 'YZ'] = dataset.slice(normal='x', origin=[x,y,z], generate_triangles=generate_triangles)
         output[1, 'XZ'] = dataset.slice(normal='y', origin=[x,y,z], generate_triangles=generate_triangles)
         output[2, 'XY'] = dataset.slice(normal='z', origin=[x,y,z], generate_triangles=generate_triangles)
@@ -242,7 +247,8 @@ class DataSetFilters(object):
 
 
     def slice_along_axis(dataset, n=5, axis='x', tolerance=None,
-                         generate_triangles=False, contour=False):
+                         generate_triangles=False, contour=False,
+                         bounds=None, center=None):
         """Create many slices of the input dataset along a specified axis.
 
         Parameters
@@ -267,7 +273,6 @@ class DataSetFilters(object):
 
         """
         axes = {'x':0, 'y':1, 'z':2}
-        output = pyvista.MultiBlock()
         if isinstance(axis, int):
             ax = axis
             axis = list(axes.keys())[list(axes.values()).index(ax)]
@@ -277,11 +282,22 @@ class DataSetFilters(object):
             except KeyError:
                 raise RuntimeError('Axis ({}) not understood'.format(axis))
         # get the locations along that axis
+        if bounds is None:
+            bounds = dataset.bounds
+        if center is None:
+            center = dataset.center
         if tolerance is None:
-            tolerance = (dataset.bounds[ax*2+1] - dataset.bounds[ax*2]) * 0.01
-        rng = np.linspace(dataset.bounds[ax*2]+tolerance, dataset.bounds[ax*2+1]-tolerance, n)
-        center = list(dataset.center)
+            tolerance = (bounds[ax*2+1] - bounds[ax*2]) * 0.01
+        rng = np.linspace(bounds[ax*2]+tolerance, bounds[ax*2+1]-tolerance, n)
+        center = list(center)
         # Make each of the slices
+        output = pyvista.MultiBlock()
+        if isinstance(dataset, pyvista.MultiBlock):
+            for i in range(dataset.n_blocks):
+                output[i] = dataset[i].slice_along_axis(n=n, axis=axis,
+                    tolerance=tolerance, generate_triangles=generate_triangles,
+                    contour=contour, bounds=bounds, center=center)
+            return output
         for i in range(n):
             center[ax] = rng[i]
             slc = DataSetFilters.slice(dataset, normal=axis, origin=center,
@@ -936,7 +952,10 @@ class DataSetFilters(object):
         alg.SetInputDataObject(dataset)
         alg.SetPassCellData(pass_cell_data)
         alg.Update()
-        return _get_output(alg, active_scalar=dataset.active_scalar_name)
+        active_scalar = None
+        if not isinstance(dataset, pyvista.MultiBlock):
+            active_scalar = dataset.active_scalar_name
+        return _get_output(alg, active_scalar=active_scalar)
 
 
     def point_data_to_cell_data(dataset, pass_point_data=False):
@@ -955,7 +974,10 @@ class DataSetFilters(object):
         alg.SetInputDataObject(dataset)
         alg.SetPassPointData(pass_point_data)
         alg.Update()
-        return _get_output(alg, active_scalar=dataset.active_scalar_name)
+        active_scalar = None
+        if not isinstance(dataset, pyvista.MultiBlock):
+            active_scalar = dataset.active_scalar_name
+        return _get_output(alg, active_scalar=active_scalar)
 
 
     def triangulate(dataset):
@@ -1420,3 +1442,114 @@ class DataSetFilters(object):
             plt.title(title)
         if show:
          return plt.show()
+
+
+
+class CompositeFilters(object):
+    """An internal class to manage filtes/algorithms for composite datasets.
+    """
+
+
+    def extract_geometry(composite):
+        """Combines the geomertry of all blocks into a single ``PolyData``
+        object. Place this filter at the end of a pipeline before a polydata
+        consumer such as a polydata mapper to extract geometry from all blocks
+        and append them to one polydata object.
+        """
+        gf = vtk.vtkCompositeDataGeometryFilter()
+        gf.SetInputData(composite)
+        gf.Update()
+        return wrap(gf.GetOutputDataObject(0))
+
+
+    def combine(composite, merge_points=False):
+        """Appends all blocks into a single unstructured grid.
+
+        Parameters
+        ----------
+        merge_points : bool, optional
+            Merge coincidental points.
+
+        """
+        alg = vtk.vtkAppendFilter()
+        for block in composite:
+            alg.AddInputData(block)
+        alg.SetMergePoints(merge_points)
+        alg.Update()
+        return wrap(alg.GetOutputDataObject(0))
+
+
+    clip = DataSetFilters.clip
+
+
+    clip_box = DataSetFilters.clip_box
+
+
+    slice = DataSetFilters.slice
+
+
+    slice_orthogonal = DataSetFilters.slice_orthogonal
+
+
+    slice_along_axis = DataSetFilters.slice_along_axis
+
+
+    slice_along_line = DataSetFilters.slice_along_line
+
+
+    wireframe = DataSetFilters.wireframe
+
+
+    elevation = DataSetFilters.elevation
+
+
+    compute_cell_sizes = DataSetFilters.compute_cell_sizes
+
+
+    cell_centers = DataSetFilters.cell_centers
+
+
+    cell_data_to_point_data = DataSetFilters.cell_data_to_point_data
+
+
+    point_data_to_cell_data = DataSetFilters.point_data_to_cell_data
+
+
+    triangulate = DataSetFilters.triangulate
+
+
+    def outline(composite, generate_faces=False, nested=False):
+        """Produces an outline of the full extent for the all blocks in this
+        composite dataset.
+
+        Parameters
+        ----------
+        generate_faces : bool, optional
+            Generate solid faces for the box. This is off by default
+
+        nested : bool, optional
+            If True, these creates individual outlines for each nested dataset
+        """
+        if nested:
+            return DataSetFilters.outline(composite, generate_faces=generate_faces)
+        box = pyvista.Box(bounds=composite.bounds)
+        return box.outline(generate_faces=generate_faces)
+
+
+    def outline_corners(composite, factor=0.2, nested=False):
+        """Produces an outline of the corners for the all blocks in this
+        composite dataset.
+
+        Parameters
+        ----------
+        factor : float, optional
+            controls the relative size of the corners to the length of the
+            corresponding bounds
+
+        ested : bool, optional
+            If True, these creates individual outlines for each nested dataset
+        """
+        if nested:
+            return DataSetFilters.outline_corners(composite, factor=factor)
+        box = pyvista.Box(bounds=composite.bounds)
+        return box.outline_corners(factor=factor)
diff --git a/pyvista/plotting.py b/pyvista/plotting.py
index d147c365..26a9c782 100644
--- a/pyvista/plotting.py
+++ b/pyvista/plotting.py
@@ -843,8 +843,9 @@ class BasePlotter(object):
                         continue # move on if we can't plot it
                 else:
                     data = mesh.GetBlock(idx)
-                if data is None:
+                if data is None or (not isinstance(data, pyvista.MultiBlock) and data.n_points < 1):
                     # Note that a block can exist but be None type
+                    # or it could have zeros points (be empty) after filtering
                     continue
                 # Now check that scalars is available for this dataset
                 if isinstance(data, vtk.vtkMultiBlockDataSet) or get_scalar(data, scalars) is None:
diff --git a/pyvista/utilities.py b/pyvista/utilities.py
index bb617bb9..0a0d4433 100644
--- a/pyvista/utilities.py
+++ b/pyvista/utilities.py
@@ -347,11 +347,13 @@ def wrap(vtkdataset):
     # Otherwise, we assume a VTK data object was passed
     if hasattr(vtkdataset, 'GetClassName'):
         key = vtkdataset.GetClassName()
+    elif vtkdataset is None:
+        return None
     else:
         raise NotImplementedError('Type ({}) not able to be wrapped into a PyVista mesh.'.format(type(vtkdataset)))
     try:
         wrapped = wrappers[key](vtkdataset)
-    except:
+    except KeyError:
         logging.warning('VTK data type ({}) is not currently supported by pyvista.'.format(key))
         return vtkdataset # if not supported just passes the VTK data object
     return wrapped


Test patch:
diff --git a/tests/test_container.py b/tests/test_composite.py
similarity index 80%
rename from tests/test_container.py
rename to tests/test_composite.py
index a04444d5..c91d9208 100644
--- a/tests/test_container.py
+++ b/tests/test_composite.py
@@ -13,20 +13,36 @@ from pyvista.plotting import system_supports_plotting
 def test_multi_block_init_vtk():
     multi = vtk.vtkMultiBlockDataSet()
     multi.SetBlock(0, vtk.vtkRectilinearGrid())
-    multi.SetBlock(1, vtk.vtkTable())
+    multi.SetBlock(1, vtk.vtkStructuredGrid())
     multi = pyvista.MultiBlock(multi)
     assert isinstance(multi, pyvista.MultiBlock)
     assert multi.n_blocks == 2
-    assert isinstance(multi[0], pyvista.RectilinearGrid)
-    assert isinstance(multi[1], vtk.vtkTable)
+    assert isinstance(multi.GetBlock(0), pyvista.RectilinearGrid)
+    assert isinstance(multi.GetBlock(1), pyvista.StructuredGrid)
     multi = vtk.vtkMultiBlockDataSet()
     multi.SetBlock(0, vtk.vtkRectilinearGrid())
-    multi.SetBlock(1, vtk.vtkTable())
+    multi.SetBlock(1, vtk.vtkStructuredGrid())
     multi = pyvista.MultiBlock(multi, deep=True)
     assert isinstance(multi, pyvista.MultiBlock)
     assert multi.n_blocks == 2
-    assert isinstance(multi[0], pyvista.RectilinearGrid)
-    assert isinstance(multi[1], vtk.vtkTable)
+    assert isinstance(multi.GetBlock(0), pyvista.RectilinearGrid)
+    assert isinstance(multi.GetBlock(1), pyvista.StructuredGrid)
+    # Test nested structure
+    multi = vtk.vtkMultiBlockDataSet()
+    multi.SetBlock(0, vtk.vtkRectilinearGrid())
+    multi.SetBlock(1, vtk.vtkImageData())
+    nested = vtk.vtkMultiBlockDataSet()
+    nested.SetBlock(0, vtk.vtkUnstructuredGrid())
+    nested.SetBlock(1, vtk.vtkStructuredGrid())
+    multi.SetBlock(2, nested)
+    # Wrap the nested structure
+    multi = pyvista.MultiBlock(multi)
+    assert isinstance(multi, pyvista.MultiBlock)
+    assert multi.n_blocks == 3
+    assert isinstance(multi.GetBlock(0), pyvista.RectilinearGrid)
+    assert isinstance(multi.GetBlock(1), pyvista.UniformGrid)
+    assert isinstance(multi.GetBlock(2), pyvista.MultiBlock)
+
 
 def test_multi_block_init_dict():
     data = dict()
@@ -36,9 +52,9 @@ def test_multi_block_init_dict():
     assert isinstance(multi, pyvista.MultiBlock)
     assert multi.n_blocks == 2
     # Note that disctionaries do not maintain order
-    assert isinstance(multi[0], (pyvista.RectilinearGrid, pyvista.PolyData))
+    assert isinstance(multi.GetBlock(0), (pyvista.RectilinearGrid, pyvista.PolyData))
     assert multi.get_block_name(0) in ['grid','poly']
-    assert isinstance(multi[1], (pyvista.RectilinearGrid, pyvista.PolyData))
+    assert isinstance(multi.GetBlock(1), (pyvista.RectilinearGrid, pyvista.PolyData))
     assert multi.get_block_name(1) in ['grid','poly']
 
 def test_multi_block_init_list():
@@ -46,8 +62,8 @@ def test_multi_block_init_list():
     multi = pyvista.MultiBlock(data)
     assert isinstance(multi, pyvista.MultiBlock)
     assert multi.n_blocks == 2
-    assert isinstance(multi[0], pyvista.RectilinearGrid)
-    assert isinstance(multi[1], pyvista.PolyData)
+    assert isinstance(multi.GetBlock(0), pyvista.RectilinearGrid)
+    assert isinstance(multi.GetBlock(1), pyvista.PolyData)
 
 
 def test_multi_block_append():
@@ -118,19 +134,19 @@ def test_multi_block_set_get_ers():
         idx = multi.get_index_by_name('foo')
 
 
-# def test_mutli_block_clean():
-#     # now test a clean of the null values
-#     multi = pyvista.MultiBlock()
-#     multi[1, 'rect'] = ex.load_rectilinear()
-#     multi[5, 'uni'] = ex.load_uniform()
-#     # perfromt he clean to remove all Null elements
-#     multi.clean()
-#     assert multi.n_blocks == 2
-#     assert multi.GetNumberOfBlocks() == 2
-#     assert isinstance(multi[0], pyvista.RectilinearGrid)
-#     assert isinstance(multi[1], pyvista.UniformGrid)
-#     assert multi.get_block_name(0) == 'rect'
-#     assert multi.get_block_name(1) == 'uni'
+def test_mutli_block_clean():
+    # now test a clean of the null values
+    multi = pyvista.MultiBlock()
+    multi[1, 'rect'] = ex.load_rectilinear()
+    multi[5, 'uni'] = ex.load_uniform()
+    # perfromt he clean to remove all Null elements
+    multi.clean()
+    assert multi.n_blocks == 2
+    assert multi.GetNumberOfBlocks() == 2
+    assert isinstance(multi[0], pyvista.RectilinearGrid)
+    assert isinstance(multi[1], pyvista.UniformGrid)
+    assert multi.get_block_name(0) == 'rect'
+    assert multi.get_block_name(1) == 'uni'
 
 
 
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 85c6a00a..296fe70d 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -4,7 +4,7 @@ import pytest
 import pyvista
 from pyvista import examples
 
-datasets = [
+DATASETS = [
     examples.load_uniform(), # UniformGrid
     examples.load_rectilinear(), # RectilinearGrid
     examples.load_hexbeam(), # UnstructuredGrid
@@ -13,20 +13,24 @@ datasets = [
 ]
 normals = ['x', 'y', '-z', (1,1,1), (3.3, 5.4, 0.8)]
 
+COMPOSITE = pyvista.MultiBlock(DATASETS, deep=True)
 
 
 def test_clip_filter():
     """This tests the clip filter on all datatypes avaialble filters"""
-    for i, dataset in enumerate(datasets):
+    for i, dataset in enumerate(DATASETS):
         clp = dataset.clip(normal=normals[i], invert=True)
         assert clp is not None
         if isinstance(dataset, pyvista.PolyData):
             assert isinstance(clp, pyvista.PolyData)
         else:
             assert isinstance(clp, pyvista.UnstructuredGrid)
+    # Now test composite data structures
+    output = COMPOSITE.clip(normal=normals[0], invert=False)
+    assert output.n_blocks == COMPOSITE.n_blocks
 
 def test_clip_box():
-    for i, dataset in enumerate(datasets):
+    for i, dataset in enumerate(DATASETS):
         clp = dataset.clip_box(invert=True)
         assert clp is not None
         assert isinstance(clp, pyvista.UnstructuredGrid)
@@ -37,35 +41,44 @@ def test_clip_box():
     result = dataset.clip_box(bounds=0.5)
     with pytest.raises(AssertionError):
         dataset.clip_box(bounds=(5, 6,))
+    # Now test composite data structures
+    output = COMPOSITE.clip_box(invert=False)
+    assert output.n_blocks == COMPOSITE.n_blocks
 
 def test_slice_filter():
     """This tests the slice filter on all datatypes avaialble filters"""
-    for i, dataset in enumerate(datasets):
+    for i, dataset in enumerate(DATASETS):
         slc = dataset.slice(normal=normals[i])
         assert slc is not None
         assert isinstance(slc, pyvista.PolyData)
     dataset = examples.load_uniform()
-    with pytest.raises(AssertionError):
-        dataset.slice(origin=(10, 15, 15))
+    result = dataset.slice(origin=(10, 15, 15))
+    assert result.n_points < 1
+    # Now test composite data structures
+    output = COMPOSITE.slice(normal=normals[0])
+    assert output.n_blocks == COMPOSITE.n_blocks
 
 
 def test_slice_orthogonal_filter():
     """This tests the slice filter on all datatypes avaialble filters"""
 
-    for i, dataset in enumerate(datasets):
+    for i, dataset in enumerate(DATASETS):
         slices = dataset.slice_orthogonal()
         assert slices is not None
         assert isinstance(slices, pyvista.MultiBlock)
         assert slices.n_blocks == 3
         for slc in slices:
             assert isinstance(slc, pyvista.PolyData)
+    # Now test composite data structures
+    output = COMPOSITE.slice_orthogonal()
+    assert output.n_blocks == COMPOSITE.n_blocks
 
 
 def test_slice_along_axis():
     """Test the many slices along axis filter """
     axii = ['x', 'y', 'z', 'y', 0]
     ns =  [2, 3, 4, 10, 20, 13]
-    for i, dataset in enumerate(datasets):
+    for i, dataset in enumerate(DATASETS):
         slices = dataset.slice_along_axis(n=ns[i], axis=axii[i])
         assert slices is not None
         assert isinstance(slices, pyvista.MultiBlock)
@@ -75,9 +88,12 @@ def test_slice_along_axis():
     dataset = examples.load_uniform()
     with pytest.raises(RuntimeError):
         dataset.slice_along_axis(axis='u')
+    # Now test composite data structures
+    output = COMPOSITE.slice_along_axis()
+    assert output.n_blocks == COMPOSITE.n_blocks
 
 def test_threshold():
-    for i, dataset in enumerate(datasets[0:3]):
+    for i, dataset in enumerate(DATASETS[0:3]):
         thresh = dataset.threshold()
         assert thresh is not None
         assert isinstance(thresh, pyvista.UnstructuredGrid)
@@ -92,9 +108,9 @@ def test_threshold():
     thresh = dataset.threshold([100, 500], invert=True)
     assert thresh is not None
     assert isinstance(thresh, pyvista.UnstructuredGrid)
-    # Now test datasets without arrays
+    # Now test DATASETS without arrays
     with pytest.raises(AssertionError):
-        for i, dataset in enumerate(datasets[3:-1]):
+        for i, dataset in enumerate(DATASETS[3:-1]):
             thresh = dataset.threshold()
             assert thresh is not None
             assert isinstance(thresh, pyvista.UnstructuredGrid)
@@ -107,7 +123,7 @@ def test_threshold_percent():
     percents = [25, 50, [18.0, 85.0], [19.0, 80.0], 0.70]
     inverts = [False, True, False, True, False]
     # Only test data sets that have arrays
-    for i, dataset in enumerate(datasets[0:3]):
+    for i, dataset in enumerate(DATASETS[0:3]):
         thresh = dataset.threshold_percent(percent=percents[i], invert=inverts[i])
         assert thresh is not None
         assert isinstance(thresh, pyvista.UnstructuredGrid)
@@ -120,29 +136,45 @@ def test_threshold_percent():
 
 
 def test_outline():
-    for i, dataset in enumerate(datasets):
+    for i, dataset in enumerate(DATASETS):
         outline = dataset.outline()
         assert outline is not None
         assert isinstance(outline, pyvista.PolyData)
+    # Now test composite data structures
+    output = COMPOSITE.outline()
+    assert isinstance(output, pyvista.PolyData)
+    output = COMPOSITE.outline(nested=True)
+    assert output.n_blocks == COMPOSITE.n_blocks
 
 def test_outline_corners():
-    for i, dataset in enumerate(datasets):
+    for i, dataset in enumerate(DATASETS):
         outline = dataset.outline_corners()
         assert outline is not None
         assert isinstance(outline, pyvista.PolyData)
+    # Now test composite data structures
+    output = COMPOSITE.outline_corners()
+    assert isinstance(output, pyvista.PolyData)
+    output = COMPOSITE.outline_corners(nested=True)
+    assert output.n_blocks == COMPOSITE.n_blocks
 
 
 def test_extract_geometry():
-    for i, dataset in enumerate(datasets):
+    for i, dataset in enumerate(DATASETS):
         outline = dataset.extract_geometry()
         assert outline is not None
         assert isinstance(outline, pyvista.PolyData)
+    # Now test composite data structures
+    output = COMPOSITE.extract_geometry()
+    assert isinstance(output, pyvista.PolyData)
 
 def test_wireframe():
-    for i, dataset in enumerate(datasets):
+    for i, dataset in enumerate(DATASETS):
         wire = dataset.wireframe()
         assert wire is not None
         assert isinstance(wire, pyvista.PolyData)
+    # Now test composite data structures
+    output = COMPOSITE.wireframe()
+    assert output.n_blocks == COMPOSITE.n_blocks
 
 
 def test_contour():
@@ -192,6 +224,9 @@ def test_elevation():
     # test errors
     with pytest.raises(RuntimeError):
         elev = dataset.elevation(scalar_range=0.5)
+    # Now test composite data structures
+    output = COMPOSITE.elevation()
+    assert output.n_blocks == COMPOSITE.n_blocks
 
 
 def test_texture_map_to_plane():
@@ -214,7 +249,7 @@ def test_texture_map_to_plane():
 
 
 def test_compute_cell_sizes():
-    for i, dataset in enumerate(datasets):
+    for i, dataset in enumerate(DATASETS):
         result = dataset.compute_cell_sizes()
         assert result is not None
         assert isinstance(result, type(dataset))
@@ -224,17 +259,23 @@ def test_compute_cell_sizes():
     grid = pyvista.UniformGrid((10,10,10))
     volume = float(np.prod(np.array(grid.dimensions) - 1))
     assert np.allclose(grid.volume, volume)
+    # Now test composite data structures
+    output = COMPOSITE.compute_cell_sizes()
+    assert output.n_blocks == COMPOSITE.n_blocks
 
 
 
 def test_cell_centers():
-    for i, dataset in enumerate(datasets):
+    for i, dataset in enumerate(DATASETS):
         result = dataset.cell_centers()
         assert result is not None
         assert isinstance(result, pyvista.PolyData)
+    # Now test composite data structures
+    output = COMPOSITE.cell_centers()
+    assert output.n_blocks == COMPOSITE.n_blocks
 
 def test_glyph():
-    for i, dataset in enumerate(datasets):
+    for i, dataset in enumerate(DATASETS):
         result = dataset.glyph()
         assert result is not None
         assert isinstance(result, pyvista.PolyData)
@@ -278,12 +319,18 @@ def test_cell_data_to_point_data():
     foo = data.cell_data_to_point_data()
     assert foo.n_arrays == 2
     assert len(foo.cell_arrays.keys()) == 0
+    # Now test composite data structures
+    output = COMPOSITE.cell_data_to_point_data()
+    assert output.n_blocks == COMPOSITE.n_blocks
 
 def test_point_data_to_cell_data():
     data = examples.load_uniform()
     foo = data.point_data_to_cell_data()
     assert foo.n_arrays == 2
     assert len(foo.point_arrays.keys()) == 0
+    # Now test composite data structures
+    output = COMPOSITE.point_data_to_cell_data()
+    assert output.n_blocks == COMPOSITE.n_blocks
 
 
 def test_triangulate():
@@ -291,6 +338,9 @@ def test_triangulate():
     tri = data.triangulate()
     assert isinstance(tri, pyvista.UnstructuredGrid)
     assert np.any(tri.cells)
+    # Now test composite data structures
+    output = COMPOSITE.triangulate()
+    assert output.n_blocks == COMPOSITE.n_blocks
 
 
 def test_delaunay_3d():
@@ -361,3 +411,9 @@ def test_slice_along_line():
     line = line2.cast_to_unstructured_grid().merge(line.cast_to_unstructured_grid())
     with pytest.raises(AssertionError):
         slc = model.slice_along_line(line)
+    # Now test composite data structures
+    a = [COMPOSITE.bounds[0], COMPOSITE.bounds[2], COMPOSITE.bounds[4]]
+    b = [COMPOSITE.bounds[1], COMPOSITE.bounds[3], COMPOSITE.bounds[5]]
+    line = pyvista.Line(a, b, resolution=10)
+    output = COMPOSITE.slice_along_line(line)
+    assert output.n_blocks == COMPOSITE.n_blocks