Repository: pyvista/pyvista

Issue:
visible cell selection ("select cells on")
From @S0Phon in https://github.com/pyvista/pyvista-support/issues/20#issue-461475637

> I have an stl file and would like to get the indices of all the cells visible from a chosen viewpoint. I know this is possible in vtk using the vtkHardwareSelector but am curious if it is possible in pyvista?

We should implement an argument to the `enable_cell_picking` method that will on select cells ON and not through.

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/pyvista/pyvista/pull/281?src=pr&el=h1) Report
> Merging [#281](https://codecov.io/gh/pyvista/pyvista/pull/281?src=pr&el=desc) into [cell-picking](https://codecov.io/gh/pyvista/pyvista/commit/3e6d9f462ee4b9315bc699cbd03aab2bd790ff8e?src=pr&el=desc) will **decrease** coverage by `0.33%`.
> The diff coverage is `7.69%`.

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

```diff
@@               Coverage Diff                @@
##           cell-picking     #281      +/-   ##
================================================
- Coverage         84.43%   84.09%   -0.34%     
================================================
  Files                30       30              
  Lines              6725     6754      +29     
================================================
+ Hits               5678     5680       +2     
- Misses             1047     1074      +27
```


| [Impacted Files](https://codecov.io/gh/pyvista/pyvista/pull/281?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pyvista/plotting/renderer.py](https://codecov.io/gh/pyvista/pyvista/pull/281/diff?src=pr&el=tree#diff-cHl2aXN0YS9wbG90dGluZy9yZW5kZXJlci5weQ==) | `92.19% <16.66%> (-1.13%)` | :arrow_down: |
| [pyvista/plotting/plotting.py](https://codecov.io/gh/pyvista/pyvista/pull/281/diff?src=pr&el=tree#diff-cHl2aXN0YS9wbG90dGluZy9wbG90dGluZy5weQ==) | `74.29% <6.06%> (-1.22%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pyvista/pyvista/pull/281?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/281?src=pr&el=footer). Last update [3e6d9f4...0c9d8c8](https://codecov.io/gh/pyvista/pyvista/pull/281?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

banesullivan: Here is an example of the mentioned bug:

```py
import pyvista as pv
from pyvista import examples

mesh = examples.load_hexbeam()

p = pv.Plotter(notebook=0)
p.add_mesh(mesh, show_edges=True, color='w')
p.enable_cell_picking(through=False)
p.show()
```

![ezgif com-video-to-gif-6](https://user-images.githubusercontent.com/22067021/60475919-d0fe3b00-9c36-11e9-93cf-71c7153f7709.gif)


Patch:
diff --git a/examples/01-filter/glyphs.py b/examples/01-filter/glyphs.py
index 1ca6d9f3..0875211b 100644
--- a/examples/01-filter/glyphs.py
+++ b/examples/01-filter/glyphs.py
@@ -53,13 +53,13 @@ vectors = np.vstack(
 sphere.vectors = vectors * 0.3
 
 # plot just the arrows
-sphere.arrows.plot()
+sphere.arrows.plot(scalars='GlyphScale')
 
 ###############################################################################
 
 # plot the arrows and the sphere
 p = pv.Plotter()
-p.add_mesh(sphere.arrows, lighting=False, stitle="Vector Magnitude")
+p.add_mesh(sphere.arrows, scalars='GlyphScale', lighting=False, stitle="Vector Magnitude")
 p.add_mesh(sphere, color="grey", ambient=0.6, opacity=0.5, show_edges=False)
 p.show()
 
@@ -70,15 +70,14 @@ p.show()
 #
 # Sometimes you might not want glyphs for every node in the input dataset. In
 # this case, you can choose to build glyphs for a subset of the input dataset
-# by using a percentage of the points. Using this percentage, a uniform
-# distribuiton is used to select points from the input dataset and use them for
-# glyphing.
+# by using a merging tolerance. Here we specify a merging tolerance of five
+# percent which equats to five perfect of the bounding box's length.
 
 # Example dataset with normals
 mesh = examples.load_random_hills()
 
 # create a subset of arrows using the glyph filter
-arrows = mesh.glyph(scale="Normals", orient="Normals", subset=0.05)
+arrows = mesh.glyph(scale="Normals", orient="Normals", tolerance=0.05)
 
 p = pv.Plotter()
 p.add_mesh(arrows, color="black")
diff --git a/pyvista/core/filters.py b/pyvista/core/filters.py
index 3fa70f05..68ec70e9 100644
--- a/pyvista/core/filters.py
+++ b/pyvista/core/filters.py
@@ -768,7 +768,7 @@ class DataSetFilters(object):
 
 
     def glyph(dataset, orient=True, scale=True, factor=1.0, geom=None,
-              subset=None):
+              tolerance=0.0, absolute=False):
         """
         Copies a geometric representation (called a glyph) to every
         point in the input dataset.  The glyph may be oriented along
@@ -789,23 +789,27 @@ class DataSetFilters(object):
         geom : vtk.vtkDataSet
             The geometry to use for the glyph
 
-        subset : float, optional
-            Take a percentage subset of the mesh's points. Float value is
-            percent subset between 0 and 1.
+        tolerance : float, optional
+            Specify tolerance in terms of fraction of bounding box length.
+            Float value is between 0 and 1. Default is 0.0. If ``absolute``
+            is ``True`` then the tolerance can be an absolute distance.
+
+        absolute : bool, optional
+            Control if ``tolerance`` is an absolute distance or a fraction.
         """
-        if subset is not None:
-            if subset <= 0.0 or subset > 1.0:
-                raise RuntimeError('subset must be a percentage between 0 and 1.')
-            ids = np.random.randint(low=0, high=dataset.n_points-1,
-                                    size=int(dataset.n_points * subset))
-            small = pyvista.PolyData(dataset.points[ids])
-            for name in dataset.point_arrays.keys():
-                small.point_arrays[name] = dataset.point_arrays[name][ids]
-            dataset = small
+        # Clean the points before glyphing
+        small = pyvista.PolyData(dataset.points)
+        small.point_arrays.update(dataset.point_arrays)
+        dataset = small.clean(point_merging=True, merge_tol=tolerance,
+                              lines_to_points=False, polys_to_lines=False,
+                              strips_to_polys=False, inplace=False,
+                              absolute=absolute)
+        # Make glyphing geometry
         if geom is None:
             arrow = vtk.vtkArrowSource()
             arrow.Update()
             geom = arrow.GetOutput()
+        # Run the algorithm
         alg = vtk.vtkGlyph3D()
         alg.SetSourceData(geom)
         if isinstance(scale, str):
diff --git a/pyvista/core/pointset.py b/pyvista/core/pointset.py
index a0be1225..536df44b 100644
--- a/pyvista/core/pointset.py
+++ b/pyvista/core/pointset.py
@@ -1267,7 +1267,8 @@ class PolyData(vtkPolyData, PointSet):
             return mesh
 
     def clean(self, point_merging=True, merge_tol=None, lines_to_points=True,
-              polys_to_lines=True, strips_to_polys=True, inplace=False):
+              polys_to_lines=True, strips_to_polys=True, inplace=False,
+              absolute=True, **kwargs):
         """
         Cleans mesh by merging duplicate points, remove unused
         points, and/or remove degenerate cells.
@@ -1279,7 +1280,9 @@ class PolyData(vtkPolyData, PointSet):
 
         merge_tol : float, optional
             Set merging tolarance.  When enabled merging is set to
-            absolute distance
+            absolute distance. If ``absolute`` is False, then the merging
+            tolerance is a fraction of the bounding box legnth. The alias
+            ``tolerance`` is also excepted.
 
         lines_to_points : bool, optional
             Turn on/off conversion of degenerate lines to points.  Enabled by
@@ -1295,18 +1298,26 @@ class PolyData(vtkPolyData, PointSet):
         inplace : bool, optional
             Updates mesh in-place while returning nothing.  Default True.
 
+        absolute : bool, optional
+            Control if ``merge_tol`` is an absolute distance or a fraction.
+
         Returns
         -------
         mesh : pyvista.PolyData
             Cleaned mesh.  None when inplace=True
         """
+        if merge_tol is None:
+            merge_tol = kwargs.pop('tolerance', None)
         clean = vtk.vtkCleanPolyData()
         clean.SetConvertLinesToPoints(lines_to_points)
         clean.SetConvertPolysToLines(polys_to_lines)
         clean.SetConvertStripsToPolys(strips_to_polys)
-        if merge_tol:
-            clean.ToleranceIsAbsoluteOn()
-            clean.SetAbsoluteTolerance(merge_tol)
+        if isinstance(merge_tol, (int, float)):
+            if absolute:
+                clean.ToleranceIsAbsoluteOn()
+                clean.SetAbsoluteTolerance(merge_tol)
+            else:
+                clean.SetTolerance(merge_tol)
         clean.SetInputData(self)
         clean.Update()
 
diff --git a/pyvista/plotting/plotting.py b/pyvista/plotting/plotting.py
index f849337b..e164784d 100644
--- a/pyvista/plotting/plotting.py
+++ b/pyvista/plotting/plotting.py
@@ -70,10 +70,14 @@ class BasePlotter(object):
         return object.__new__(cls)
 
     def __init__(self, shape=(1, 1), border=None, border_color='k',
-                 border_width=1.0):
+                 border_width=1.0, title=None):
         """ Initialize base plotter """
         self.image_transparent_background = rcParams['transparent_background']
 
+        if title is None:
+            title = rcParams['title']
+        self.title = str(title)
+
         # by default add border for multiple plots
         if border is None:
             if shape != (1, 1):
@@ -2854,37 +2858,46 @@ class BasePlotter(object):
             self.remove_actor(self.legend, reset_camera=False)
             self._render()
 
-    def enable_cell_picking(self, mesh=None, callback=None, show=True,
-                            show_message=True, style='wireframe', line_width=5,
-                            color='pink', font_size=18, **kwargs):
+    def get_pick_position(self):
+        """Get the pick position/area as x0, y0, x1, y1"""
+        return self.renderer.get_pick_position()
+
+    def enable_cell_picking(self, mesh=None, callback=None, through=True,
+                            show=True, show_message=True, style='wireframe',
+                            line_width=5, color='pink', font_size=18, **kwargs):
         """
         Enables picking of cells.  Press r to enable retangle based
         selection.  Press "r" again to turn it off.  Selection will be
         saved to self.picked_cells.
-
         Uses last input mesh for input
 
+        Warning
+        -------
+        Visible cell picking (``through=False``) is known to not perfrom well
+        and produce incorrect selections on non-triangulated meshes if using
+        any grpahics card other than NVIDIA. A warning will be thrown if the
+        mesh is not purely triangles when using visible cell selection.
+
         Parameters
         ----------
         mesh : vtk.UnstructuredGrid, optional
             UnstructuredGrid grid to select cells from.  Uses last
             input grid by default.
-
         callback : function, optional
             When input, calls this function after a selection is made.
             The picked_cells are input as the first parameter to this function.
-
+        through : bool, optional
+            When True (default) the picker will select all cells through the
+            mesh. When False, the picker will select only visible cells on the
+            mesh's surface.
         show : bool
             Show the selection interactively
-
         show_message : bool, str
             Show the message about how to use the cell picking tool. If this
             is a string, that will be the message shown.
-
         kwargs : optional
             All remaining keyword arguments are used to control how the
             selection is intereactively displayed
-
         """
         if hasattr(self, 'notebook') and self.notebook:
             raise AssertionError('Cell picking not available in notebook plotting')
@@ -2895,18 +2908,11 @@ class BasePlotter(object):
             mesh = self.mesh
 
 
-        def pick_call_back(picker, event_id):
-            extract = vtk.vtkExtractGeometry()
-            mesh.cell_arrays['orig_extract_id'] = np.arange(mesh.n_cells)
-            extract.SetInputData(mesh)
-            extract.SetImplicitFunction(picker.GetFrustum())
-            extract.Update()
-            self.picked_cells = pyvista.wrap(extract.GetOutput())
-
+        def end_pick_helper(picker, event_id):
             if show:
                 # Use try incase selection is empty
                 try:
-                    self.add_mesh(self.picked_cells, name='cell_picking_selection',
+                    self.add_mesh(self.picked_cells, name='_cell_picking_selection',
                         style=style, color=color, line_width=line_width, **kwargs)
                 except RuntimeError:
                     pass
@@ -2915,9 +2921,56 @@ class BasePlotter(object):
                 callback(self.picked_cells)
 
             # TODO: Deactivate selection tool
+            return
+
+
+        def through_pick_call_back(picker, event_id):
+            extract = vtk.vtkExtractGeometry()
+            mesh.cell_arrays['orig_extract_id'] = np.arange(mesh.n_cells)
+            extract.SetInputData(mesh)
+            extract.SetImplicitFunction(picker.GetFrustum())
+            extract.Update()
+            self.picked_cells = pyvista.wrap(extract.GetOutput())
+            return end_pick_helper(picker, event_id)
+
+
+        def visible_pick_call_back(picker, event_id):
+            x0,y0,x1,y1 = self.get_pick_position()
+            selector = vtk.vtkOpenGLHardwareSelector()
+            selector.SetFieldAssociation(vtk.vtkDataObject.FIELD_ASSOCIATION_CELLS)
+            selector.SetRenderer(self.renderer)
+            selector.SetArea(x0,y0,x1,y1)
+            cellids = selector.Select().GetNode(0)
+            if cellids is None:
+                # No selection
+                return
+            selection = vtk.vtkSelection()
+            selection.AddNode(cellids)
+            extract = vtk.vtkExtractSelectedIds()
+            extract.SetInputData(0, mesh)
+            extract.SetInputData(1, selection)
+            extract.Update()
+            self.picked_cells = pyvista.wrap(extract.GetOutput())
+            return end_pick_helper(picker, event_id)
+
 
-        area_picker = vtk.vtkAreaPicker()
-        area_picker.AddObserver(vtk.vtkCommand.EndPickEvent, pick_call_back)
+        area_picker = vtk.vtkRenderedAreaPicker()
+        if through:
+            area_picker.AddObserver(vtk.vtkCommand.EndPickEvent, through_pick_call_back)
+        else:
+            # check if mesh is triangulated or not
+            # Reference:
+            #     https://github.com/pyvista/pyvista/issues/277
+            #     https://github.com/pyvista/pyvista/pull/281
+            message = "Surface picking non-triangulated meshes is known to "\
+                      "not work properly with non-NVIDIA GPUs. Please "\
+                      "consider triangulating your mesh:\n"\
+                      "\t`.extract_geometry().tri_filter()`"
+            if (not isinstance(mesh, pyvista.PolyData) or
+                    mesh.faces.size % 4 or
+                    not np.all(mesh.faces.reshape(-1, 4)[:,0] == 3)):
+                logging.warning(message)
+            area_picker.AddObserver(vtk.vtkCommand.EndPickEvent, visible_pick_call_back)
 
         self.enable_rubber_band_style()
         self.iren.SetPicker(area_picker)
@@ -2926,7 +2979,7 @@ class BasePlotter(object):
         if show_message:
             if show_message == True:
                 show_message = "Press R to toggle selection tool"
-            self.add_text(str(show_message), font_size=font_size)
+            self.add_text(str(show_message), font_size=font_size, name='_cell_picking_message')
         return
 
 
@@ -3201,8 +3254,12 @@ class Plotter(BasePlotter):
                     renderer.camera_position = cpos
             self._first_time = False
 
+        if title is None:
+            title = self.title
+
         if title:
             self.ren_win.SetWindowName(title)
+            self.title = title
 
         # if full_screen:
         if full_screen:
diff --git a/pyvista/plotting/qt_plotting.py b/pyvista/plotting/qt_plotting.py
index 38533d3f..66ae72ca 100644
--- a/pyvista/plotting/qt_plotting.py
+++ b/pyvista/plotting/qt_plotting.py
@@ -284,7 +284,7 @@ class QtInteractor(QVTKRenderWindowInteractor, BasePlotter):
         if not has_pyqt:
             raise AssertionError('Requires PyQt5')
         QVTKRenderWindowInteractor.__init__(self, parent)
-        BasePlotter.__init__(self, shape=shape)
+        BasePlotter.__init__(self, shape=shape, title=title)
         self.parent = parent
 
         # Create and start the interactive renderer
@@ -295,7 +295,7 @@ class QtInteractor(QVTKRenderWindowInteractor, BasePlotter):
 
         self.background_color = rcParams['background']
 
-        if title:
+        if self.title:
             self.setWindowTitle(title)
 
         self.iren.RemoveObservers('MouseMoveEvent')  # slows window update?
@@ -382,7 +382,9 @@ class BackgroundPlotter(QtInteractor):
         view_menu.addAction('Clear All', self.clear)
 
         tool_menu = main_menu.addMenu('Tools')
-        tool_menu.addAction('Enable Cell Picking (r-key)', self.enable_cell_picking)
+
+        tool_menu.addAction('Enable Cell Picking (through)', self.enable_cell_picking)
+        tool_menu.addAction('Enable Cell Picking (visible)', lambda: self.enable_cell_picking(through=False))
 
         cam_menu = view_menu.addMenu('Camera')
         cam_menu.addAction('Reset Camera', self.reset_camera)
diff --git a/pyvista/plotting/renderer.py b/pyvista/plotting/renderer.py
index 93042da2..e4925993 100644
--- a/pyvista/plotting/renderer.py
+++ b/pyvista/plotting/renderer.py
@@ -787,6 +787,15 @@ class Renderer(vtkRenderer):
         return
 
 
+    def get_pick_position(self):
+        """Get the pick position/area as x0, y0, x1, y1"""
+        x0 = int(self.GetPickX1())
+        x1 = int(self.GetPickX2())
+        y0 = int(self.GetPickY1())
+        y1 = int(self.GetPickY2())
+        return x0, y0, x1, y1
+
+
 
 def _remove_mapper_from_plotter(plotter, actor, reset_camera):
     """removes this actor's mapper from the given plotter's _scalar_bar_mappers"""
diff --git a/pyvista/plotting/theme.py b/pyvista/plotting/theme.py
index 109f0c02..3435ddda 100644
--- a/pyvista/plotting/theme.py
+++ b/pyvista/plotting/theme.py
@@ -50,7 +50,8 @@ rcParams = {
     'interactive' : False,
     'render_points_as_spheres' : False,
     'use_panel' : True,
-    'transparent_background' : False
+    'transparent_background' : False,
+    'title' : 'PyVista',
 }
 
 DEFAULT_THEME = dict(rcParams)


Test patch:
diff --git a/tests/test_filters.py b/tests/test_filters.py
index e17f7a7d..e2bee58a 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -294,7 +294,7 @@ def test_glyph():
     sphere.point_arrays['arr'] = np.ones(sphere.n_points)
     result = sphere.glyph(scale='arr')
     result = sphere.glyph(scale='arr', orient='Normals', factor=0.1)
-    result = sphere.glyph(scale='arr', orient='Normals', factor=0.1, subset=0.5)
+    result = sphere.glyph(scale='arr', orient='Normals', factor=0.1, tolerance=0.1)
 
 
 def test_split_and_connectivity():