Repository: pyvista/pyvista

Issue:
`find_cells_along_line` not working according to description
### Discussed in https://github.com/pyvista/pyvista/discussions/4390

<div type='discussions-op-text'>

<sup>Originally posted by **AsimAl-Sofi** May  6, 2023</sup>
Hi all
I would like to use the function **find_cells_along_line** to determine the cells along a line, but the function does not give me the right answer.
Here is my code example:
```py
out = pv.CylinderStructured(radius=(0,3),
theta_resolution=10,height=1,
z_resolution=2)
box=out.get_cell(5).get_face(3).cast_to_unstructured_grid().extract_surface()
box.compute_normals(inplace=True)
centers = out.get_cell(5).get_face(3).cast_to_unstructured_grid().cell_centers().points
arrows = box['Normals'][0]
pl = pv.Plotter()
pl.add_mesh(out, opacity=0.6, show_edges=True, line_width=1)
pl.add_points(arrows,color="g", point_size=18.0, render_points_as_spheres=True)
point_S = (0, -5, 2)
pl.add_points(np.array([point_S]),color="r", point_size=18.0, render_points_as_spheres=True)
line=pv.Line(point_S, arrows)
pl.add_mesh(line)
pl.show()
```

the function find_cells_along_line gives me the following answer:
```py
>>> out.find_cells_along_line(point_S, arrows,tolerance=0.0)
array([1, 0, 2, 7, 8, 9])
```
As you can see from the example, the line goes through cell 0, 8 and 9 but the function gives 1, 0, 2, 7, 8, 9.
Any suggestions how to tackle this problem?
Thanks</div>

Discussion:
MatthewFlamm: cc @AsimAl-Sofi if you want to test this PR
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pyvista/pyvista/pull/4399?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) Report
> Merging [#4399](https://app.codecov.io/gh/pyvista/pyvista/pull/4399?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) (e726a00) into [main](https://app.codecov.io/gh/pyvista/pyvista/commit/3e9be97314827724a4c39d05a5d13d922fe4f39e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) (3e9be97) will **decrease** coverage by `0.05%`.
> The diff coverage is `84.61%`.

```diff
@@            Coverage Diff             @@
##             main    #4399      +/-   ##
==========================================
- Coverage   95.82%   95.78%   -0.05%     
==========================================
  Files          97       97              
  Lines       20874    20886      +12     
==========================================
+ Hits        20002    20005       +3     
- Misses        872      881       +9     
```


Patch:
diff --git a/examples/02-plot/gif.py b/examples/02-plot/gif.py
index 1c6d1b3b..3c79b9e9 100644
--- a/examples/02-plot/gif.py
+++ b/examples/02-plot/gif.py
@@ -10,11 +10,18 @@ Generate a moving gif from an active plotter.
    "jittery" GIFs, especially for the scalar bar.
 
 """
+# sphinx_gallery_thumbnail_number = 2
 
 import numpy as np
 
 import pyvista as pv
 
+###############################################################################
+# Create a structured grid
+# ~~~~~~~~~~~~~~~~~~~~~~~~
+# Create a structured grid and make a "wave" my shifting the Z position based
+# on the cartesian distance from the origin.
+
 x = np.arange(-10, 10, 0.5)
 y = np.arange(-10, 10, 0.5)
 x, y = np.meshgrid(x, y)
@@ -23,6 +30,13 @@ z = np.sin(r)
 
 # Create and structured surface
 grid = pv.StructuredGrid(x, y, z)
+grid.plot()
+
+
+###############################################################################
+# Generate a GIF
+# ~~~~~~~~~~~~~~
+# Generate a GIF using ``off_screen=True`` parameter.
 
 # Create a plotter object and set the scalars to the Z height
 plotter = pv.Plotter(notebook=False, off_screen=True)
diff --git a/pyvista/core/dataset.py b/pyvista/core/dataset.py
index faa9d391..14643264 100644
--- a/pyvista/core/dataset.py
+++ b/pyvista/core/dataset.py
@@ -25,6 +25,7 @@ import numpy as np
 
 import pyvista
 from pyvista import _vtk
+from pyvista.core.errors import VTKVersionError
 from pyvista.utilities import (
     FieldAssociation,
     abstract_class,
@@ -2419,7 +2420,7 @@ class DataSet(DataSetFilters, DataObject):
         pointb: Iterable[float],
         tolerance=0.0,
     ) -> np.ndarray:
-        """Find the index of cells in this mesh along a line.
+        """Find the index of cells whose bounds intersect a line.
 
         Line is defined from ``pointa`` to ``pointb``.
 
@@ -2431,14 +2432,21 @@ class DataSet(DataSetFilters, DataObject):
         pointb : sequence[float]
             Length 3 coordinate of the end of the line.
 
-        tolerance : float, default: False
+        tolerance : float, default: 0.0
             The absolute tolerance to use to find cells along line.
 
         Returns
         -------
         numpy.ndarray
-            Index or indices of the cell in this mesh that are closest
-            to the given point.
+            Index or indices of the cell(s) whose bounds intersect
+            the line.
+
+        Warnings
+        --------
+        This method returns cells whose bounds intersect the line.
+        This means that the line may not intersect the cell itself.
+        To obtain cells that intersect the line, use
+        :func:`pyvista.DataSet.find_cells_intersecting_line`.
 
         See Also
         --------
@@ -2446,12 +2454,14 @@ class DataSet(DataSetFilters, DataObject):
         DataSet.find_closest_cell
         DataSet.find_containing_cell
         DataSet.find_cells_within_bounds
+        DataSet.find_cells_intersecting_line
 
         Examples
         --------
         >>> import pyvista
         >>> mesh = pyvista.Sphere()
-        >>> index = mesh.find_cells_along_line([0, 0, 0], [0, 0, 1.0])
+        >>> mesh.find_cells_along_line([0.0, 0, 0], [1.0, 0, 0])
+        array([842, 843, 896, 897])
 
         """
         if np.array(pointa).size != 3:
@@ -2465,6 +2475,66 @@ class DataSet(DataSetFilters, DataObject):
         locator.FindCellsAlongLine(pointa, pointb, tolerance, id_list)
         return vtk_id_list_to_array(id_list)
 
+    def find_cells_intersecting_line(
+        self,
+        pointa: Iterable[float],
+        pointb: Iterable[float],
+        tolerance=0.0,
+    ) -> np.ndarray:
+        """Find the index of cells that intersect a line.
+
+        Line is defined from ``pointa`` to ``pointb``.  This
+        method requires vtk version >=9.2.0.
+
+        Parameters
+        ----------
+        pointa : sequence[float]
+            Length 3 coordinate of the start of the line.
+
+        pointb : sequence[float]
+            Length 3 coordinate of the end of the line.
+
+        tolerance : float, default: 0.0
+            The absolute tolerance to use to find cells along line.
+
+        Returns
+        -------
+        numpy.ndarray
+            Index or indices of the cell(s) that intersect
+            the line.
+
+        See Also
+        --------
+        DataSet.find_closest_point
+        DataSet.find_closest_cell
+        DataSet.find_containing_cell
+        DataSet.find_cells_within_bounds
+        DataSet.find_cells_along_line
+
+        Examples
+        --------
+        >>> import pyvista
+        >>> mesh = pyvista.Sphere()
+        >>> mesh.find_cells_intersecting_line([0.0, 0, 0], [1.0, 0, 0])
+        array([896])
+
+        """
+        if pyvista.vtk_version_info < (9, 2, 0):
+            raise VTKVersionError("pyvista.PointSet requires VTK >= 9.2.0")
+
+        if np.array(pointa).size != 3:
+            raise TypeError("Point A must be a length three tuple of floats.")
+        if np.array(pointb).size != 3:
+            raise TypeError("Point B must be a length three tuple of floats.")
+        locator = _vtk.vtkCellLocator()
+        locator.SetDataSet(self)
+        locator.BuildLocator()
+        id_list = _vtk.vtkIdList()
+        points = _vtk.vtkPoints()
+        cell = _vtk.vtkGenericCell()
+        locator.IntersectWithLine(pointa, pointb, tolerance, points, id_list, cell)
+        return vtk_id_list_to_array(id_list)
+
     def find_cells_within_bounds(self, bounds: Iterable[float]) -> np.ndarray:
         """Find the index of cells in this mesh within bounds.
 
diff --git a/pyvista/utilities/sphinx_gallery.py b/pyvista/utilities/sphinx_gallery.py
index de14f974..ebae4064 100644
--- a/pyvista/utilities/sphinx_gallery.py
+++ b/pyvista/utilities/sphinx_gallery.py
@@ -55,6 +55,7 @@ class Scraper:
             fname = next(image_path_iterator)
             if hasattr(plotter, '_gif_filename'):
                 # move gif to fname
+                fname = fname[:-3] + "gif"
                 shutil.move(plotter._gif_filename, fname)
             else:
                 plotter.screenshot(fname)
diff --git a/requirements_docs.txt b/requirements_docs.txt
index d0caa674..16b68081 100644
--- a/requirements_docs.txt
+++ b/requirements_docs.txt
@@ -10,7 +10,7 @@ jupyter_sphinx==0.4.0
 jupyterlab==3.6.3
 lxml==4.9.2
 meshio==5.3.4
-mypy==1.2.0
+mypy==1.3.0
 mypy-extensions==1.0.0
 numpydoc==1.5.0
 osmnx==1.3.0
@@ -30,8 +30,8 @@ sphinx-notfound-page==0.8.3
 sphinx-toolbox==3.4.0
 sphinxcontrib-asciinema==0.3.7
 sphinxcontrib-websupport==1.2.4
-sympy==1.11.1
-trame==2.4.0
+sympy==1.12
+trame==2.4.1
 trame-client==2.7.5
 trame-server==2.11.0
 trame-vtk==2.4.4


Test patch:
diff --git a/requirements_test.txt b/requirements_test.txt
index 45675cd1..0b80c164 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -23,7 +23,7 @@ pytest_pyvista==0.1.8
 pythreejs<2.5.0
 Sphinx<6.3.0
 sphinx-gallery<0.14.0
-sympy<1.12.0
+sympy<1.13.0
 tqdm<4.66.0
 trame>=2.2.6,<2.5
 trame-client>=2.5.1,<2.8
diff --git a/tests/test_dataset.py b/tests/test_dataset.py
index 47558bf9..ba928337 100644
--- a/tests/test_dataset.py
+++ b/tests/test_dataset.py
@@ -975,6 +975,28 @@ def test_find_cells_along_line():
     assert len(indices) == 2
 
 
+def test_find_cells_intersecting_line():
+    if pyvista.vtk_version_info >= (9, 2, 0):
+        mesh = pyvista.Sphere()
+        indices = mesh.find_cells_intersecting_line([0, 0, 0.0], [1.0, 0, 0.0])
+        assert len(indices) == 1
+
+        indices = mesh.find_cells_intersecting_line([0, 0, 0.0], [1.0, 0, 0.0], tolerance=0.01)
+        assert len(indices) == 2
+
+        with pytest.raises(TypeError):
+            mesh.find_cells_intersecting_line([0, 0], [1.0, 0, 0.0])
+
+        with pytest.raises(TypeError):
+            mesh.find_cells_intersecting_line([0, 0, 0.0], [1.0, 0])
+
+    else:
+        with pytest.raises(VTKVersionError):
+            mesh = pyvista.Sphere()
+            indices = mesh.find_cells_intersecting_line([0, 0, 0.0], [1.0, 0, 0.0])
+            assert len(indices) == 1
+
+
 def test_find_cells_within_bounds():
     mesh = pyvista.Cube()
 
diff --git a/tests/test_scraper.py b/tests/test_scraper.py
index bb8ca04f..dd199c9e 100644
--- a/tests/test_scraper.py
+++ b/tests/test_scraper.py
@@ -18,12 +18,22 @@ def test_scraper(tmpdir, monkeypatch, n_win):
     monkeypatch.setattr(pyvista, 'BUILDING_GALLERY', True)
     pyvista.close_all()
     plotters = [pyvista.Plotter(off_screen=True) for _ in range(n_win)]
+    plotter_gif = pyvista.Plotter()
+
     scraper = Scraper()
     src_dir = str(tmpdir)
     out_dir = op.join(str(tmpdir), '_build', 'html')
+
     img_fnames = [
         op.join(src_dir, 'auto_examples', 'images', f'sg_img_{n}.png') for n in range(n_win)
     ]
+
+    # create and save GIF to tmpdir
+    gif_path = op.abspath(tmpdir + 'sg_img_0.gif')
+    plotter_gif.open_gif(gif_path)
+    plotter_gif.write_frame()
+    plotter_gif.close()
+
     gallery_conf = {"src_dir": src_dir, "builder_name": "html"}
     target_file = op.join(src_dir, 'auto_examples', 'sg.py')
     block = None
@@ -32,9 +42,13 @@ def test_scraper(tmpdir, monkeypatch, n_win):
         example_globals=dict(a=1),
         target_file=target_file,
     )
+
     os.makedirs(op.dirname(img_fnames[0]))
     for img_fname in img_fnames:
         assert not os.path.isfile(img_fname)
+
+    # add gif to list after checking other filenames are empty
+    img_fnames.append(gif_path)
     os.makedirs(out_dir)
     scraper(block, block_vars, gallery_conf)
     for img_fname in img_fnames: