CGAL 6.1 - CGAL Basic Viewer

Loading...
Searching...
No Matches
User Manual

Author
  Guillaume Damiand and Mostafa Ashraf

# Introduction

The basic viewer package provides interactive visualization for most CGAL packages, such as 2D Arrangements, 2D Boolean Set, Linear cell complex, 3D Nef, 2D Periodic Triangulations, 3D Point Set, 2D Polygon, 3D Polyhedral Surface, 2D Straight Skeleton, Surface Mesh, 2D Triangulation, 3D Triangulation, 2D Voronoi Diagram, and more.

The most simple use case of the basic viewer is the call of the global `draw()` function. There is one draw function for each CGAL package. Such a call opens a new interactive window showing the given model and allowing to navigate in the scene, show or hide some specific cells, show the interior of the model if any, etc.

The drawing can be tuned using `Graphics_scene_options`, for example to change the color of some cells or hide some cells.

More complex usage can be achieved by using the `Graphics_scene` class. Different CGAL data structures can be added in a same `Graphics_scene` allowing to visualize them simultaneously in a same window. Like for the draw function, the drawing parameters can be tuned thanks to the `Graphics_scene_options`.

Two classes are based on Qt.

- `CGAL::Qt::Basic_viewer` is a Qt widget that inherits from `QGLViewer` (libQGLViewer is an open source C++ library based on Qt that eases the creation of OpenGL 3D viewers).
- `CGAL::Qt::QApplication_and_basic_viewer` which regroups a `Qt::QApplication` and a `CGAL::Qt::Basic_viewer`.

# Software Design

The diagram in Figure 121.1 shows the main classes and functions of this package.

Figure 121.1 UML diagram of the classes and functions of the package (only partial representation).

# Basic Usage: The Global Draw Functions

A first simple solution provided to draw the different data structures of CGAL is the global `draw()` function. This function is templated by the type of the data structure to draw. The following example shows how it can be used.

File Basic_viewer/draw_lcc.cpp

#include <CGAL/Linear_cell_complex_for_combinatorial_map.h>
#include <CGAL/draw_linear_cell_complex.h>
using LCC=CGAL::Linear_cell_complex_for_combinatorial_map<3>;
using Point=LCC::Point;
int main()
{
LCC lcc;
lcc.make_hexahedron(Point(0,0,0), Point(5,0,0),
Point(5,5,0), Point(0,5,0),
Point(0,5,4), Point(0,0,4),
Point(5,0,4), Point(5,5,4));
CGAL::draw(lcc);
return EXIT_SUCCESS;
}
CGAL::Linear_cell_complex_for_combinatorial_map
CGAL::draw
void draw(const PS2 &ps2, const GSOptions &gso)

This example creates a 3D linear cell complex of a hexahedron, and draws it. The result is shown in the following figure.

Figure 121.2 Example of drawing of a 3D LCC.

Users can interact with the viewer using some specific keys or mouse interactions. Pressing key 'h' makes pop up a help window showing the different shortcuts and mouse interactions. The main functionalities of the viewer are:

- Versatile Rendering:

  The viewer supports the rendering of various geometric elements, such as points, edges, faces, rays, and lines. Different rendering modes, including mono and colored representation, are available for these elements.

- Camera Control:

  The viewer allows users to move the camera, switch between 2D and 3D viewing modes, adjusting the camera accordingly. Camera settings, such as orthographic or perspective projection, can be configured based on the dimension of the scene.

- User Interaction:

  Users can interact with the viewer through keyboard inputs, enabling them to control rendering options, toggle the display of elements, and adjust visual parameters. Key presses are mapped to specific actions, such as toggling the clipping plane, changing rendering modes, adjusting the size of elements, and modifying ambient light color.

- Clipping Plane:

  The viewer includes support for a clipping plane, enabling users to selectively render parts of the scene. The clipping plane can be toggled on and off, and its rendering style can be modified (solid, wireframe, etc.).

## Tuning with Graphics Scene Options

There is one specialization of each draw function that takes graphics scene options as additional parameter, allowing to tune the drawing.

The `Graphics_scene_options` class provides a set of options and customization parameters for rendering geometric structures in a graphics scene. Its main purpose is to allow users to control the visual appearance of various cells such as vertices, edges, faces, and volumes in a graphical representation of a given data structure.

The following example shows how to use graphics scene options to tune the drawing of a surface mesh. We define our own class `My_graphics_scene_options` that inherits from `Graphics_scene_options` to get all the default parameters. In this class, we only override the two methods `colored_vertex` and `vertex_color` to draw all vertices in color, and chose randomly green or blue colors for one out of two vertices.

File Basic_viewer/draw_surface_mesh_vcolor.cpp

#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/draw_surface_mesh.h>
#include <CGAL/Graphics_scene_options.h>
#include <fstream>
using Kernel=CGAL::Simple_cartesian<double>;
using Point=Kernel::Point_3;
using Mesh=CGAL::Surface_mesh<Point>;
// Inherit from CGAL::Graphics_scene_options to get all the default values.
struct My_graphics_scene_options:
public CGAL::Graphics_scene_options<Mesh,
typename boost::graph_traits<Mesh>::vertex_descriptor,
typename boost::graph_traits<Mesh>::edge_descriptor,
typename boost::graph_traits<Mesh>::face_descriptor>
{
// All vertices are colored.
bool colored_vertex(const Mesh&,
typename boost::graph_traits<Mesh>::vertex_descriptor) const
{ return true; }
// Change the color of vertices randomly.
CGAL::IO::Color vertex_color(const Mesh&,
typename boost::graph_traits<Mesh>::vertex_descriptor) const
{
static bool v_green=true;
v_green=!v_green;
if(v_green) // 1 vertex out of two green (randomly)
{ return CGAL::IO::Color(0,220,0); }
else // the others are blue
{ return CGAL::IO::Color(0,0,220); }
}
};
int main(int argc, char* argv[])
{
const std::string filename = (argc>1) ? argv[1] : CGAL::data_file_path("meshes/elephant.off");
Mesh sm;
if(!CGAL::IO::read_polygon_mesh(filename, sm))
{
std::cerr << "Invalid input file: " << filename << std::endl;
return EXIT_FAILURE;
}
// Draw the mesh using the new graphics scene option.
CGAL::draw(sm, My_graphics_scene_options());
return EXIT_SUCCESS;
}
CGAL::IO::Color
CGAL::Surface_mesh
Kernel::Point_3
Kernel
CGAL::Graphics_scene_options
The class Graphics_scene_options is used to tune the way that the cells of a given data structure of ...
Definition: Graphics_scene_options.h:28
CGAL::Simple_cartesian

The result of this example can be shown in Figure 121.3.

Figure 121.3 Example of tuned drawing of a 3D surface mesh where some vertex colors are changed.

The drawing options can depend on the cells. In the following example the color of each face depends on its height.

File Basic_viewer/draw_surface_mesh_height.cpp

#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/draw_surface_mesh.h>
#include <CGAL/Graphics_scene_options.h>
#include <iostream>
typedef CGAL::Simple_cartesian<double> Kernel;
typedef Kernel::Point_3 Point;
typedef CGAL::Surface_mesh<Point> Mesh;
struct Colored_faces_given_height:
public CGAL::Graphics_scene_options<Mesh,
typename Mesh::Vertex_index,
typename Mesh::Edge_index,
typename Mesh::Face_index>
{
Colored_faces_given_height(const Mesh& sm)
{
if(sm.is_empty()) return;
double m_min_y=0., m_max_y=0.;
bool first=true;
for(typename Mesh::Vertex_index vi: sm.vertices())
{
if(first)
{ m_min_y=sm.point(vi).y(); m_max_y=m_min_y; first=false; }
else
{
m_min_y=(std::min)(m_min_y, sm.point(vi).y());
m_max_y=(std::max)(m_max_y, sm.point(vi).y());
}
}
this->colored_face=[](const Mesh &, typename Mesh::Face_index)->bool { return true; };
this->face_color=[m_min_y, m_max_y]
(const Mesh& sm, typename Mesh::Face_index fi)->CGAL::IO::Color
{
double res=0.;
std::size_t n=0;
for(typename Mesh::Vertex_index vi: vertices_around_face(sm.halfedge(fi), sm))
{
res+=sm.point(vi).y();
++n;
}
// Random color depending on the "height" of the facet
CGAL::Random random(static_cast<unsigned int>(30*((res/n)-m_min_y)/(m_max_y-m_min_y)));
return CGAL::get_random_color(random);
};
}
};
int main(int argc, char* argv[])
{
const std::string filename = (argc>1) ? argv[1] : CGAL::data_file_path("meshes/elephant.off");
Mesh sm;
if(!CGAL::IO::read_polygon_mesh(filename, sm))
{
std::cerr << "Invalid input file: " << filename << std::endl;
return EXIT_FAILURE;
}
CGAL::draw(sm, Colored_faces_given_height(sm));
return EXIT_SUCCESS;
}

The result of this example is shown in Figure 121.4.

Figure 121.4 Example of mesh drawing with a color for each face computed depending on its height.

# The Graphics Scene

It is possible to do more advanced visualizations by using the class `Graphics_scene`. It is a container class for various geometric elements, such as points, segments, rays, lines, faces and texts. It provides several methods to add elements, possibly with individual colors. Things are a little bit different for faces. You must call `face_begin()` to start a new face (possibly with a color), add all the points in the face by using `add_point_in_face()`, and call `face_end()` to finish the face. Note that the given polygonal face is internally triangulated.

This class also stores the normal of the faces (computed automatically), and possibly the normal of the vertices that can be provided by the users.

Users can either fill directly a `Graphics_scene` using these methods, or by using a global function `add_to_graphics_scene()` that fills the scene using all the geometric elements of a given CGAL data structure. Like for the draw functions, there is an overload of `add_to_graphics_scene()` for each CGAL package.

A `Graphics_scene` can be drawn using the basic viewer thanks to the `draw_graphics_scene()` function.

The following example shows in a same viewer both a `Point_set_3` and a `Polyhedron_3`. Note that, like the draw functions, each `add_to_graphics_scene()` can use an optional `Graphics_scene_options` as parameter allowing to tune the drawing of the given data structure. In the example, we change the color of all vertices of the point set.

File Basic_viewer/draw_mesh_and_points.cpp

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Polyhedron_3.h>
#include <CGAL/poisson_surface_reconstruction.h>
#include <CGAL/IO/read_points.h>
#include <CGAL/draw_polyhedron.h>
#include <CGAL/draw_point_set_3.h>
#include <CGAL/Graphics_scene_options.h>
#include <CGAL/Basic_viewer.h>
#include <vector>
#include <iostream>
using Kernel=CGAL::Exact_predicates_inexact_constructions_kernel;
using Point=Kernel::Point_3;
using Vector=Kernel::Vector_3;
using Pwn=std::pair<Point, Vector>;
using Polyhedron=CGAL::Polyhedron_3<Kernel>;
using PS3=CGAL::Point_set_3<Point>;
struct Graphics_scene_options_green_points:
public CGAL::Graphics_scene_options<PS3, typename PS3::const_iterator,
typename PS3::const_iterator,
typename PS3::const_iterator>
{
bool colored_vertex(const PS3&, typename PS3::const_iterator) const
{ return true; }
CGAL::IO::Color vertex_color(const PS3&, typename PS3::const_iterator) const
{ return CGAL::IO::Color(0,220,0); }
};
int main(void)
{
std::vector<Pwn> points;
if(!CGAL::IO::read_points(CGAL::data_file_path("points_3/kitten.xyz"), std::back_inserter(points),
CGAL::parameters::point_map(CGAL::First_of_pair_property_map<Pwn>())
.normal_map(CGAL::Second_of_pair_property_map<Pwn>())))
{
std::cerr << "Error: cannot read input file " << CGAL::data_file_path("points_3/kitten.xyz") << std::endl;
return EXIT_FAILURE;
}
Polyhedron output_mesh;
double average_spacing = CGAL::compute_average_spacing<CGAL::Sequential_tag>
(points, 6, CGAL::parameters::point_map(CGAL::First_of_pair_property_map<Pwn>()));
if (CGAL::poisson_surface_reconstruction_delaunay
(points.begin(), points.end(),
CGAL::First_of_pair_property_map<Pwn>(),
CGAL::Second_of_pair_property_map<Pwn>(),
output_mesh, average_spacing))
{
PS3 point_set;
for(Pwn& it: points)
{ point_set.insert(it.first); }
CGAL::Graphics_scene scene;
CGAL::add_to_graphics_scene(point_set, scene, Graphics_scene_options_green_points());
CGAL::add_to_graphics_scene(output_mesh, scene);
CGAL::draw_graphics_scene(scene);
}
else
{ return EXIT_FAILURE; }
return EXIT_SUCCESS;
}
CGAL::Exact_predicates_inexact_constructions_kernel
CGAL::Graphics_scene
The class Graphics_scene stores points, segments, triangles, rays, and lines.
Definition: Graphics_scene.h:10
CGAL::Point_set_3
CGAL::Polyhedron_3
Kernel::Vector_3
CGAL::draw_graphics_scene
void draw_graphics_scene(const Graphics_scene &graphic_scene, const char *title="CGAL Basic Viewer")
opens a new window and draws the given Graphics_scene (which must have been filled before).
Definition: Basic_viewer.h:235
CGAL::add_to_graphics_scene
void add_to_graphics_scene(const PS2 &ps2, CGAL::Graphics_scene &gs, const GSOptions &gso)

The result of this example is shown in Figure 121.5 where we can see in green the original points of the point cloud, superposed with the polyhedron surface reconstructed by the Poisson surface reconstruction method.

Figure 121.5 Example of drawing of a point cloud and a polyhedron in a same viewer.

# The Qt Basic Viewer Class

The class `CGAL::Qt::Basic_viewer` is a Qt widget that inherits from `QGLViewer` and mainly stores a `Graphics_scene` and allows to visualize it and interact with the scene. Since this class is a Qt widget, it can be used into more complex Qt code to create more advanced demos.

In the following example, we create two graphics scenes, one filled with a point cloud, a second one filled with the polyhedral surface reconstructed by the Poisson surface reconstruction method. Then, we create two basic viewers associated with these two scenes. The two basic viewers are added into a Qt layout allowing to visualize the two scenes side by side.

File Basic_viewer/draw_several_windows.cpp

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Polyhedron_3.h>
#include <CGAL/poisson_surface_reconstruction.h>
#include <CGAL/IO/read_points.h>
#include <CGAL/draw_polyhedron.h>
#include <CGAL/draw_point_set_3.h>
#include <CGAL/Graphics_scene_options.h>
#include <CGAL/Basic_viewer.h>
#ifdef CGAL_USE_BASIC_VIEWER
#include <QMainWindow>
#endif
#include <vector>
#include <iostream>
using Kernel=CGAL::Exact_predicates_inexact_constructions_kernel;
using Point=Kernel::Point_3;
using Vector=Kernel::Vector_3;
using Pwn=std::pair<Point, Vector>;
using Polyhedron=CGAL::Polyhedron_3<Kernel>;
using PS3=CGAL::Point_set_3<Point>;
int main(void)
{
std::vector<Pwn> points;
if(!CGAL::IO::read_points(CGAL::data_file_path("points_3/kitten.xyz"), std::back_inserter(points),
CGAL::parameters::point_map(CGAL::First_of_pair_property_map<Pwn>())
.normal_map(CGAL::Second_of_pair_property_map<Pwn>())))
{
std::cerr << "Error: cannot read input file " << CGAL::data_file_path("points_3/kitten.xyz") << std::endl;
return EXIT_FAILURE;
}
Polyhedron output_mesh;
double average_spacing = CGAL::compute_average_spacing<CGAL::Sequential_tag>
(points, 6, CGAL::parameters::point_map(CGAL::First_of_pair_property_map<Pwn>()));
if (!CGAL::poisson_surface_reconstruction_delaunay
(points.begin(), points.end(),
CGAL::First_of_pair_property_map<Pwn>(),
CGAL::Second_of_pair_property_map<Pwn>(),
output_mesh, average_spacing))
{ return EXIT_FAILURE; }
PS3 point_set;
for(Pwn& it: points)
{ point_set.insert(it.first); }
CGAL::Graphics_scene scene1, scene2;
CGAL::add_to_graphics_scene(point_set, scene1);
CGAL::add_to_graphics_scene(output_mesh, scene2);
#ifdef CGAL_USE_BASIC_VIEWER
#if defined(CGAL_TEST_SUITE)
bool cgal_test_suite=true;
#else
bool cgal_test_suite=qEnvironmentVariableIsSet("CGAL_TEST_SUITE");
#endif
if (cgal_test_suite) { return EXIT_SUCCESS; }
int argc=1;
const char* argv[2]={"Draw several windows example","\0"};
QApplication app(argc,const_cast<char**>(argv));
QMainWindow* mainWindow=new QMainWindow;
QWidget *centralWidget = new QWidget(mainWindow);
QHBoxLayout* layout = new QHBoxLayout(mainWindow);
CGAL::Qt::Basic_viewer bv1(mainWindow, scene1);
CGAL::Qt::Basic_viewer bv2(mainWindow, scene2);
bv1.draw_vertices(true);
layout->addWidget(&bv1);
layout->addWidget(&bv2);
centralWidget->setLayout(layout);
mainWindow->setCentralWidget(centralWidget);
mainWindow->show();
app.exec();
#endif
return EXIT_SUCCESS;
}
CGAL::Qt::Basic_viewer
The class Basic_viewer is a Qt widget based on QGLViewer that allows to visualize 3D elements: points...
Definition: Basic_viewer.h:13

The result of this example is shown in Figure 121.6.

Figure 121.6 Example of drawing of two `Basic_viewer` side by side.

# Adding Interaction

In the previous examples, the models are only drawn once and there is no interaction with the user to update the drawing.

It is possible to define such interactions thanks to the class `CGAL::Qt::QApplication_and_basic_viewer`. In this class, you can define your own function that is called automatically by the viewer when a user presses a key. This can be used to change some parameters and update the drawing accordingly.

We illustrate this possibility in the following example that shows a surface mesh, while coloring the small faces in red. To do so, we use our own graphics scene options than change the color of a face depending on its size. A face is considered small if its size is below a certain threshold. This threshold can be updated by the user, pressing key 'I' to increase it and 'D' to decrease it. This is done in the key pressed function defined in the `QApplication_and_basic_viewer`. When the threshold changes, the graphics scene is recomputed to take the modification of the size threshold into account.

File Basic_viewer/draw_surface_mesh_small_faces.cpp

#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Polygon_mesh_processing/measure.h>
#include <CGAL/Polygon_mesh_processing/triangulate_faces.h>
#include <iostream>
#include <string>
#include <cassert>
#include <CGAL/draw_surface_mesh.h>
typedef CGAL::Simple_cartesian<double> K;
typedef CGAL::Surface_mesh<K::Point_3> Mesh;
typedef Mesh::Vertex_index vertex_descriptor;
typedef Mesh::Face_index face_descriptor;
typedef K::FT FT;
template<class SM>
struct Graphics_scene_options_small_faces:
public CGAL::Graphics_scene_options<SM,
typename boost::graph_traits<SM>::vertex_descriptor,
typename boost::graph_traits<SM>::edge_descriptor,
typename boost::graph_traits<SM>::face_descriptor>
{
using Base=CGAL::Graphics_scene_options<SM,
typename boost::graph_traits<SM>::vertex_descriptor,
typename boost::graph_traits<SM>::edge_descriptor,
typename boost::graph_traits<SM>::face_descriptor>;
Graphics_scene_options_small_faces(const SM& sm): Base(), m_sm(sm)
{
std::optional<typename SM::template Property_map<face_descriptor, FT>> faces_size
= sm.template property_map<face_descriptor, FT>("f:size");
m_with_size = faces_size.has_value();
if(!m_with_size)
{ return; }
m_min_size=faces_size.value()[*(sm.faces().begin())];
m_max_size=m_min_size;
FT cur_size;
for (typename SM::Face_range::iterator f=sm.faces().begin(); f!=sm.faces().end(); ++f)
{
cur_size=faces_size.value()[*f];
if (cur_size<m_min_size) m_min_size=cur_size;
if (cur_size>m_max_size) m_max_size=cur_size;
}
this->face_color=[this](const SM& sm,
typename boost::graph_traits<SM>::face_descriptor fh) -> CGAL::IO::Color
{ return this->get_face_color(sm, fh); };
this->colored_face = [](const SM&,
typename boost::graph_traits<SM>::face_descriptor) -> bool
{ return true; };
}
CGAL::IO::Color get_face_color(const SM& sm,
typename boost::graph_traits<SM>::face_descriptor fh)
{
// Default color of faces
CGAL::IO::Color c(75,160,255);
if(!m_with_size) { return c; }
// Compare the size of the face with the % m_threshold
std::optional<typename SM::template Property_map<face_descriptor, FT>> faces_size
= sm.template property_map<face_descriptor, FT>("f:size");
assert(faces_size.has_value());
// If the face is small, color it in red.
if (get(faces_size.value(), fh)<m_min_size + ((m_max_size - m_min_size) / (100 - m_threshold)))
{ return CGAL::IO::Color(255,20,20); }
return c; // Default color
}
const SM& m_sm;
bool m_with_size;
FT m_min_size, m_max_size;
unsigned int m_threshold=85; // 85%
};
int main(int argc, char* argv[])
{
const std::string filename = (argc>1) ? argv[1] : CGAL::data_file_path("meshes/elephant.off");
Mesh sm;
if(!CGAL::IO::read_polygon_mesh(filename, sm))
{
std::cerr << "Invalid input file: " << filename << std::endl;
return EXIT_FAILURE;
}
CGAL::Polygon_mesh_processing::triangulate_faces(sm);
Mesh::Property_map<face_descriptor, FT> faces_size;
bool created;
std::tie(faces_size, created)=sm.add_property_map<face_descriptor, FT>("f:size",0.);
assert(created);
for(face_descriptor fd : sm.faces())
{ faces_size[fd]=CGAL::Polygon_mesh_processing::face_area(fd, sm); }
Graphics_scene_options_small_faces gsosm(sm);
CGAL::Graphics_scene gs;
add_to_graphics_scene(sm, gs, gsosm);
#ifdef CGAL_USE_BASIC_VIEWER
CGAL::Qt::QApplication_and_basic_viewer app(gs, "Small faces");
if(app)
{
app.basic_viewer().on_key_pressed=
[&sm, &gsosm, &gs] (QKeyEvent* e, CGAL::Qt::Basic_viewer* basic_viewer) -> bool
{
const ::Qt::KeyboardModifiers modifiers = e->modifiers();
if ((e->key() == ::Qt::Key_I) && (modifiers == ::Qt::NoButton))
{
gsosm.m_threshold+=5;
if(gsosm.m_threshold>100) { gsosm.m_threshold=100; }
basic_viewer->displayMessage
(QString("Small faces threshold=%1.").arg(gsosm.m_threshold));
gs.clear();
add_to_graphics_scene(sm, gs, gsosm);
basic_viewer->redraw();
}
else if ((e->key() == ::Qt::Key_D) && (modifiers == ::Qt::NoButton))
{
if(gsosm.m_threshold<5) { gsosm.m_threshold=0; }
else { gsosm.m_threshold-=5; }
basic_viewer->displayMessage
(QString("Small faces threshold=%1.").arg(gsosm.m_threshold));
gs.clear();
add_to_graphics_scene(sm, gs, gsosm);
basic_viewer->redraw();
}
else
{
// Return false will call the base method to process others/classicals key
return false;
}
return true;
};
// Here we add shortcut descriptions
app.basic_viewer().setKeyDescription(::Qt::Key_I, "Increase threshold for small faces");
app.basic_viewer().setKeyDescription(::Qt::Key_D, "Decrease threshold for small faces");
// Then we run the app
app.run();
}
#endif
sm.remove_property_map(faces_size);
return EXIT_SUCCESS;
}
CGAL::Graphics_scene::clear
void clear()
clears the scene, i.e., removes all points, segments, triangles, and text.
CGAL::Qt::QApplication_and_basic_viewer
The class QApplication_and_basic_viewer regroups a Basic_viewer and Qt QApplication.
Definition: Basic_viewer.h:214

The result of this example is shown in Figure 121.7, showing to the left the initial drawing of the 3D model, and to the right the same model after having changed the size threshold.

Figure 121.7 Two examples of drawing of a mesh with small faces in red. Left: With the initial threshold. Right: After having increased the threshold.

# Design and Implementation History

This package was started by Mostafa Ashraf during his 2022 GSoC project. Guillaume Damiand, who mentored the project, reworked large parts of the package, wrote examples and the manual.