230 lines
7.8 KiB
Python
230 lines
7.8 KiB
Python
|
|
"""Utilities for using pyvista with sphinx-gallery."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from pathlib import Path
|
||
|
|
import shutil
|
||
|
|
from typing import TYPE_CHECKING
|
||
|
|
|
||
|
|
import pyvista
|
||
|
|
from pyvista._deprecate_positional_args import _deprecate_positional_args
|
||
|
|
|
||
|
|
if TYPE_CHECKING:
|
||
|
|
from collections.abc import Iterator
|
||
|
|
|
||
|
|
BUILDING_GALLERY_ERROR_MSG = (
|
||
|
|
'pyvista.BUILDING_GALLERY must be set to True in your conf.py to capture '
|
||
|
|
'images within sphinx_gallery or when building documentation using the '
|
||
|
|
'pyvista-plot directive.'
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _get_sg_image_scraper():
|
||
|
|
"""Return the callable scraper to be used by Sphinx-Gallery.
|
||
|
|
|
||
|
|
It allows PyVista users to just use strings as they already can for
|
||
|
|
'matplotlib' and 'mayavi'. Details on this implementation can be found in
|
||
|
|
`sphinx-gallery/sphinx-gallery/494`_
|
||
|
|
|
||
|
|
This must be imported into the top level namespace of PyVista.
|
||
|
|
|
||
|
|
.. _sphinx-gallery/sphinx-gallery/494: https://github.com/sphinx-gallery/sphinx-gallery/pull/494
|
||
|
|
"""
|
||
|
|
return Scraper()
|
||
|
|
|
||
|
|
|
||
|
|
def html_rst(
|
||
|
|
figure_list,
|
||
|
|
sources_dir,
|
||
|
|
srcsetpaths=None,
|
||
|
|
): # pragma: no cover # numpydoc ignore=PR01,RT01
|
||
|
|
"""Generate reST for viewer with exported scene."""
|
||
|
|
from sphinx_gallery.scrapers import _get_srcset_st # noqa: PLC0415
|
||
|
|
from sphinx_gallery.scrapers import figure_rst # noqa: PLC0415
|
||
|
|
|
||
|
|
if srcsetpaths is None:
|
||
|
|
# this should never happen, but figure_rst is public, so
|
||
|
|
# this has to be a kwarg...
|
||
|
|
srcsetpaths = [{0: fl} for fl in figure_list]
|
||
|
|
|
||
|
|
images_rst = ''
|
||
|
|
for i, hinnames in enumerate(srcsetpaths):
|
||
|
|
srcset = _get_srcset_st(sources_dir, hinnames)
|
||
|
|
if srcset[-5:] == 'vtksz':
|
||
|
|
png_file = figure_list[i][:-5] + 'png'
|
||
|
|
|
||
|
|
indented_firgure_rst = '\n'.join(
|
||
|
|
' ' * 5 + line for line in figure_rst([png_file], sources_dir).split('\n')
|
||
|
|
)
|
||
|
|
images_rst += f"""
|
||
|
|
\n
|
||
|
|
\n
|
||
|
|
.. tab-set::\n
|
||
|
|
\n
|
||
|
|
.. tab-item:: Static Scene\n
|
||
|
|
\n
|
||
|
|
{indented_firgure_rst}
|
||
|
|
\n
|
||
|
|
.. tab-item:: Interactive Scene\n
|
||
|
|
\n
|
||
|
|
.. offlineviewer:: {figure_list[i]}\n\n"""
|
||
|
|
|
||
|
|
else:
|
||
|
|
images_rst += '\n' + figure_rst([figure_list[i]], sources_dir) + '\n\n'
|
||
|
|
|
||
|
|
return images_rst
|
||
|
|
|
||
|
|
|
||
|
|
def _process_events_before_scraping(plotter):
|
||
|
|
"""Process events such as changing the camera or an object before scraping."""
|
||
|
|
if plotter.iren is not None and plotter.iren.initialized:
|
||
|
|
# check for pyvistaqt app which can be specifically bound to pyvista plotter
|
||
|
|
# objects in order to interact with qt, then process the events from qt
|
||
|
|
if hasattr(plotter, 'app') and plotter.app is not None:
|
||
|
|
plotter.app.processEvents()
|
||
|
|
plotter.update()
|
||
|
|
|
||
|
|
|
||
|
|
@_deprecate_positional_args(allowed=['image_path_iterator'])
|
||
|
|
def generate_images(image_path_iterator: Iterator[str], dynamic: bool = False) -> list[str]: # noqa: FBT001, FBT002
|
||
|
|
"""Generate images from the current plotters.
|
||
|
|
|
||
|
|
The file names are taken from the ``image_path_iterator`` iterator.
|
||
|
|
|
||
|
|
A gif will be created if a plotter has a ``_gif_filename`` attribute.
|
||
|
|
Otherwise, depending on the value of ``dynamic``, either a ``.png`` static image
|
||
|
|
or a ``.vtksz`` file will be created.
|
||
|
|
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
image_path_iterator : Iterator[str]
|
||
|
|
An iterator that yields the path to the next image to be saved.
|
||
|
|
|
||
|
|
dynamic : bool, default: False
|
||
|
|
Whether to save a static ``.png`` image or a ``.vtksz`` (interactive)
|
||
|
|
file.
|
||
|
|
|
||
|
|
Returns
|
||
|
|
-------
|
||
|
|
list[str]
|
||
|
|
A list of the names of the images that were created.
|
||
|
|
|
||
|
|
"""
|
||
|
|
image_names = []
|
||
|
|
figures = pyvista.plotting.plotter._ALL_PLOTTERS
|
||
|
|
for plotter in figures.values():
|
||
|
|
_process_events_before_scraping(plotter)
|
||
|
|
fname = next(image_path_iterator)
|
||
|
|
# Make sure the extension is "png"
|
||
|
|
path = Path(fname)
|
||
|
|
fname_withoutextension = str(path.parent / path.stem)
|
||
|
|
fname = fname_withoutextension + '.png'
|
||
|
|
|
||
|
|
if (gif_filename := plotter._gif_filename) is not None:
|
||
|
|
# move gif to fname
|
||
|
|
fname = fname[:-3] + 'gif'
|
||
|
|
shutil.move(gif_filename, fname)
|
||
|
|
image_names.append(fname)
|
||
|
|
else:
|
||
|
|
plotter.screenshot(fname)
|
||
|
|
if not dynamic or plotter.last_vtksz is None:
|
||
|
|
image_names.append(fname)
|
||
|
|
else: # pragma: no cover
|
||
|
|
fname = fname[:-3] + 'vtksz'
|
||
|
|
with Path(fname).open('wb') as f:
|
||
|
|
f.write(plotter.last_vtksz) # type: ignore[arg-type]
|
||
|
|
image_names.append(fname)
|
||
|
|
|
||
|
|
pyvista.close_all() # close and clear all plotters
|
||
|
|
return image_names
|
||
|
|
|
||
|
|
|
||
|
|
class Scraper:
|
||
|
|
"""Save ``pyvista.Plotter`` objects.
|
||
|
|
|
||
|
|
Used by sphinx-gallery to generate the plots from the code in the examples.
|
||
|
|
|
||
|
|
Pass an instance of this class to ``sphinx_gallery_conf`` in your
|
||
|
|
``conf.py`` as the ``"image_scrapers"`` argument.
|
||
|
|
|
||
|
|
Be sure to set ``pyvista.BUILDING_GALLERY = True`` in your ``conf.py``.
|
||
|
|
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __repr__(self) -> str:
|
||
|
|
"""Return a stable representation of the class instance."""
|
||
|
|
return f'<{type(self).__name__} object>'
|
||
|
|
|
||
|
|
def __call__(self, block, block_vars, gallery_conf): # noqa: ARG002
|
||
|
|
"""Save the figures generated after running example code.
|
||
|
|
|
||
|
|
Called by sphinx-gallery.
|
||
|
|
|
||
|
|
"""
|
||
|
|
from sphinx_gallery.scrapers import figure_rst # noqa: PLC0415
|
||
|
|
|
||
|
|
if not pyvista.BUILDING_GALLERY:
|
||
|
|
raise RuntimeError(BUILDING_GALLERY_ERROR_MSG)
|
||
|
|
|
||
|
|
image_path_iterator = block_vars['image_path_iterator']
|
||
|
|
image_names = generate_images(image_path_iterator, dynamic=False)
|
||
|
|
return figure_rst(image_names, gallery_conf['src_dir'])
|
||
|
|
|
||
|
|
|
||
|
|
class DynamicScraper: # pragma: no cover
|
||
|
|
"""Save ``pyvista.Plotter`` objects dynamically.
|
||
|
|
|
||
|
|
Used by sphinx-gallery to generate the plots from the code in the examples.
|
||
|
|
|
||
|
|
Pass an instance of this class to ``sphinx_gallery_conf`` in your
|
||
|
|
``conf.py`` as the ``"image_scrapers"`` argument.
|
||
|
|
|
||
|
|
Be sure to set ``pyvista.BUILDING_GALLERY = True`` in your ``conf.py``.
|
||
|
|
|
||
|
|
If the boolean variable ``PYVISTA_GALLERY_FORCE_STATIC_IN_DOCUMENT = True/False``
|
||
|
|
is set as a global variable in the document then its value will be used as default for the
|
||
|
|
force_static argument of the pyvista-plot command. see also the notes at :func:plot_directive
|
||
|
|
|
||
|
|
To alter the global value behavior just for some plots you may set the
|
||
|
|
boolean variable ``PYVISTA_GALLERY_FORCE_STATIC = True``/
|
||
|
|
``PYVISTA_GALLERY_FORCE_STATIC = False`` just before the appropriate ``plot`` command.
|
||
|
|
|
||
|
|
The default behavior of this scraper is to create interactive plots.
|
||
|
|
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __repr__(self) -> str:
|
||
|
|
"""Return a stable representation of the class instance."""
|
||
|
|
return f'<{type(self).__name__} object>'
|
||
|
|
|
||
|
|
def __call__(self, block, block_vars, gallery_conf): # pragma: no cover
|
||
|
|
"""Save the figures generated after running example code.
|
||
|
|
|
||
|
|
Called by sphinx-gallery.
|
||
|
|
|
||
|
|
"""
|
||
|
|
if not pyvista.BUILDING_GALLERY:
|
||
|
|
raise RuntimeError(BUILDING_GALLERY_ERROR_MSG)
|
||
|
|
|
||
|
|
# read global option if it exists
|
||
|
|
force_static = block_vars['example_globals'].get(
|
||
|
|
'PYVISTA_GALLERY_FORCE_STATIC_IN_DOCUMENT',
|
||
|
|
False,
|
||
|
|
)
|
||
|
|
# override with block specific value if it exists
|
||
|
|
if 'PYVISTA_GALLERY_FORCE_STATIC = True' in block[1].split('\n'):
|
||
|
|
force_static = True
|
||
|
|
elif 'PYVISTA_GALLERY_FORCE_STATIC = False' in block[1].split('\n'):
|
||
|
|
force_static = False
|
||
|
|
|
||
|
|
if force_static is None:
|
||
|
|
# Just in case force_static is None at this point
|
||
|
|
force_static = False
|
||
|
|
|
||
|
|
dynamic = not force_static
|
||
|
|
|
||
|
|
image_path_iterator = block_vars['image_path_iterator']
|
||
|
|
image_names = generate_images(image_path_iterator, dynamic=dynamic)
|
||
|
|
|
||
|
|
return html_rst(image_names, gallery_conf['src_dir'])
|