7.2. Writing a C++ Process

From C++, a process is a single class inheriting one of three classes: StackProcess, MeshProcess or GlobalProcess. Which class the process inherits from will decide where in the GUI it will appear and what other processes it can call. In short, if inheriting from StackProcess or MeshProcess only processes of the same type can be called, while inheriting from GlobalProcess allows a process to call any other process.

A process’s class should follow the following pattern, in which Some must be replaced by Stack, Mesh or Global

class ProcessName : public SomeProcess {
public:
  ProcessName(const SomeProcess& process)
    : Process(process)
    , SomeProcess(process)
  {
  }

  // The two following methods are mandatory

  bool operator()(const ParmList& parms) override {
    // Implementation of the process
  }
  QString name() const override {
    return "[Process Name]";
  }

  // All other methods are optional

  bool initialize(ParmList& parms, QWidget* parent) override {
    // Provide user interface to adjust the list of parameters
    // Note: the number of parameters cannot change!
  }

  QString description() const override {
    return "[Process Description]";
  }
  QString folder() const override {
    return "[Folder in which the process is stored]";
  }
  QStringList parmNames() const override {
    return QStringList() << "Parm1"
                         << "Parm2"
                         << ...;
  }
  QStringList parmDescs() const override {
    return QStringList() << "Description Parm1"
                         << "Description Parm2"
                         << ...;
  }
  ParmList parmDefaults() const override {
    return ParmList() << value1
                      << value2
                      << ...;
  }
  ParmChoiceMap parmChoice() const override {
    ParmChoiceMap map;
    map[ParmNum1] = QStringList() << "Value 1"
                                  << "Value 2"
                                  << ...;
    map[ParmNum2] = QStringList() << "Value 1"
                                  << "Value 2"
                                  << ...;
    // ...
    return map;
  }
  QIcon icon() const override {
    return QIcon(":/path/to/resource");
  }
}

When writing the process, the first task is to parse the parameters. Then, it is customary to provide another operator() method for direct call from C++ with the parameters already parsed.

7.2.1. Process Execution

Processes are executed in a separate thread, from which you cannot run any GUI-related events. This means you must never try to provide a user interface from within a process main method. If you want a user interface, it must be implemented in the Process::initialize(...) method.

During the process execution, there are two ways to show the user the progress of the algorithm:

  1. Use the lgx::Progress class
  2. Use the lgx::process::Process::updateState() method. This will pause the process while LithoGraphX updates the view, showing the current state. Be careful for the mesh to be in a proper state when you call this function. Also, all changed states will be reset after that.

7.2.2. Parameter Parsing

The ParmList type is an alias for a QList or QVariant. When using types, remember that the user must be able to type values as a string. QVariant are quite good at converting from string to various values, except for boolean values. So if you need boolean values, You should use the parmToBool function, which will convert the strings “yes”, “y”, “true”, “t”, “on” or “1” as true and any other value as false.

7.2.3. Calling other processes

There are two ways to call other processes, depending on the availability of the C++ definition or not. The more general method consist in using the runProcess method of the Process class. The signation of the function is:

bool Process::runProcess(const QString &processType, const QString &processName, const ParmList &parms)

Launch a process by name

Note that any exception launched by the process will be filtered and converted into an error message and the process returning false.

If the C++ implementation is available (through a library for exemple), then it is possible to directly instanciate the process, which allows to call the internal operator() method. Say you want to use the SieveFilter, you can write (see include/SieveFilter/SieveFilter.hpp in the system processes folder for details):

SieveFilter filter(*this);
filter(input, output, "Median", 100.f, false);

7.2.4. Modifying the State

When a process modifies the state (e.g. image, mesh, transformation, …), it needs to inform the system. For performance reasons, it would be impractical to either track all changes or to re-analyse all the data structures when coming back from a process. This section will explain first how images and meshes are represented and then how to inform the system about what has been changed.

7.2.4.3. Image Modifications

Images are modified through the lgx::Store objects. If the image is modified, you can call one of two functions:

void lgx::Store::changed()

Call this function for large-scale changed in the image, requiring the whole image to be updated.

void lgx::Store::changed(const BoundingBox3i &bbox)

Call this function for localised changes. The bounding box should include all the voxel changed.

Other aspects of image rendering (size, transfer function, …) are small and simple enough for their modifications to be tracked automatically.

7.2.4.4. Meshes Modifications

There are five different functions to mark what changed in a mesh:

void lgx::Mesh::updateTriangles()

Call this function if triangles properties have change, which means label or heat map.

void lgx::Mesh::updateLines()

Call this function if the “lines” change. This means mostly cell or mesh edges.

void lgx::Mesh::updatePositions()

Call this function if the position of the vertices have changed.

void lgx::Mesh::updateSelection()

Call this function if the selection has changed.

void lgx::Mesh::updateAll()

Call this function if you would have called all the others or if you added/removed vertices.

7.2.5. Useful Utilities

Here are a few features you might want to look at in the developer documentation:

  • File src/Dir.hpp for path manipulations you really need to use functions here to manage paths, in particular to save paths in files.
  • File src/Geometry.hpp and class templates lgx::util::Vector and lgx::util::Matrix for geometry
  • Class lgx::util::CSVStream to parse CSV files
  • Class lgx::util::PlyFile to parse PLY files (e.g. mesh files)
  • To run a process over a whole collection use the functions in src/Algorithms.hpp.
  • Use the class lgx::Progress to provide a progress bar, possibly with a mean for the user to cancel the process.
  • Use the class lgx::Image5D and associated functions to load/save images.