Implementing an STMK operator

SMTK allows you to write operators that expose those provided by an underlying modeling kernel as well as operators that provide new functionality.

This tutorial will cover writing an operator that provides new functionality: specifically, we will create an operator that counts the number of top-level cells in a model. Given a non-default option, it may instead count the number of top-level groups in a model.

Operators in SMTK consist of 3 components:

  • a class that implements the action of the operator, as a subclass of smtk::model::Operator.

  • an SMTK attribute definition that describes the parameters the operator accepts (both required and optional)

  • an SMTK attribute definition that describes information returned by the operator.

The sections below detail each of these.

Subclassing smtk::model::Operator

We will name our example operator the CounterOperator and place it in the “ex” example namespace.

 1namespace ex
 2{
 3
 4class CounterOperation : public smtk::operation::XMLOperation
 5{
 6public:
 7  smtkTypeMacro(ex::CounterOperation);
 8  smtkCreateMacro(CounterOperation);
 9  smtkSharedFromThisMacro(smtk::operation::XMLOperation);
10  smtkSuperclassMacro(Operation);
11  // ...

Our operator is a subclass of smtk::model::Operator, which is managed using shared pointers (it uses the smtkEnableSharedPtr macro to inherit enable_shared_from_this), so we use the smtkCreateMacro to provide a static method for creating a shared pointer to a new instance and the smtkSharedFromThisMacro to override the base class’ shared_from_this() method.

Beyond these basic requirements, an operator should implement two inherited virtual methods

  • ableToOperate which is an opportunity for an operator to perform checks on the validity of input parameters that cannot be easily encoded using the attribute resource; and

  • operateInternal which implements the actual behavior of the operator.

Because our operator is simple, we implement smtk::operation::Operation::ableToOperate() in the class header file. We only declare smtk::operation::Operation::operateInternal():

1protected:
2  Result operateInternal() override;
3  const char* xmlDescription() const override;

By calling Operator::ensureSpecification in Operator::ableToOperate, we force the attribute resource to build an attribute instance which holds specifications for each parameter of this instance of the Operator. Then in Operator::operateInternal we can refer to the specification without having to verify that it is non-null; an operator’s Operator::operateInternal method will never be called unless Operator::ableToOperate returns true.

The attribute constructed by Operator::ensureSpecification is built using XML definitions we provide. We will cover the format of the XML definitions immediately below and then continue with the implementation of the operation.

Defining operator input parameters

The XML we used to declare the operator’s parameters and results uses SMTK’s attribute definition system. All operators should be derived definitions whose base definition (BaseType) is “operator”:

 1    <include href="smtk/operation/Operation.xml"/>
 2    <AttDef Type="counter" BaseType="operation">
 3      <ItemDefinitions>
 4        <Component Name="model" NumberOfRequiredValues="1">
 5          <Accepts><Resource Name="smtk::model::Resource" Filter="model|anydim"/></Accepts>
 6        </Component>
 7        <Int Name="count groups" NumberOfRequiredValues="1">
 8          <DefaultValue>0</DefaultValue>
 9        </Int>
10      </ItemDefinitions>
11    </AttDef>

Inheriting the base definition allows us to use to attribute system to easily enumerate the list of all operators.

Also, you can see in the example that our operator takes a single integer parameter named “count groups”. Its default value is 0, indicating that top-level cells (not groups) will be counted by default. We could also have made “count groups” a VoidItem and tested for whether the attribute was enabled instead of testing its value.

In the future, operators will have their “primary” operand expressed as an association: model topology to serve as the primary operand will be associated with an instance of the operator rather than declared as an item owned by the operator attribute. This will simplify use cases where an active selection exists and the user wishes to perform an operation on it; operators that can be associated with the selection will be enabled (while others will be disabled). Any further parameters may be specified after the user initiates the operation.

Defining operator output parameters

The XML description of an operator is not complete until both the input and output parameters have been specified. The output parameters are expressed as another smtk::attribute::Attribute instance, this time defined as by inheriting the “result” BaseType. The result base type includes an integer item named “outcome” use to store one of the values in the OperatorOutcome enum. The outcome indicates whether the operation was successful or not.

1    <!-- Result -->
2    <include href="smtk/operation/Result.xml"/>
3    <AttDef Type="result(counter)" BaseType="result">
4      <ItemDefinitions>
5        <Int Name="count" NumberOfRequiredValues="1">
6        </Int>
7      </ItemDefinitions>
8    </AttDef>

In addition to the outcome, our edge-counting operator also returns the number of edges it counted. Often, SMTK operators will return lists of new, modified, or removed model entities in one or more smtk::attribute::ModelEntityItem instances.

Both the input and output XML are typically maintained together in a single XML file.

Implementing the actual operation

Now that we have input and output parameters specified, the implementation of Operator::operateInternal can simply fetch items from an instance of an attribute defined by the XML:

 1smtk::operation::XMLOperation::Result CounterOperation::operateInternal()
 2{
 3  // Get the attribute holding parameter values:
 4  auto params = this->parameters();
 5
 6  // Get the input model to be processed:
 7  Model model = params->findComponent("model")->valueAs<smtk::model::Entity>();
 8
 9  // Decide whether we should count cells or groups
10  // of the model:
11  int countGroups = params->findInt("count groups")->value();
12
13  // Create the attribute holding the results of
14  // our operation using a convenience method
15  // provided by the Operation base class.
16  // Our operation is simple; we always succeed.
17  auto result = this->createResult(smtk::operation::Operation::Outcome::SUCCEEDED);
18
19  // Fetch the item to store our output:
20  smtk::attribute::IntItemPtr cellCount = result->findInt("count");
21
22  cellCount->setValue(
23    countGroups ? static_cast<int>(model.groups().size()) : static_cast<int>(model.cells().size()));
24
25  return result;
26}

Note that the base class provides a method to create a result attribute for you with the “outcome” parameter set to a value you specify.

In addition to implementing the operation, the only other thing you must do is register the operator with the proper session. This is done using the smtkImplementsModelOperator macro:

1// Implement virtual overrides from base class to handle registration.
2const char* CounterOperation::xmlDescription() const
3{
4  return implement_an_operator_xml;
5}

Warning

This macro must always be invoked in the global namespace and your operator’s class name fully qualified with the namespace in which it lives.

This macro is very important because it ties several names together:

  • the C++ class name (ex::CounterOperator),

  • the names and XML descriptions of the attribute definitions for input parameters (“counter”) and result parameters “result(counter)”, and

  • the SMTK component name used by the autoinitialization facility to force components (such as this operator) to be registered when initializing static variables. In this case the component name “ex_counter” can be used with the smtkComponentInitMacro() by adding this line:

    smtkComponentInitMacro(smtk_ex_counter_operator)
    

    to a compilation unit containing code that will be run by your application. Whenever the compilation unit’s static variables are initialized, the operator will be registered with the session class and any sessions constructed afterwards will provide the operator.

    Note that “smtk_” and “_operator” wrap the name you pass to the smtkImplementsModelOperator macro.

To make maintaining the XML simpler, SMTK provides a macro for encoding an XML file as a variable in a C++ header. The variable implement_an_operator_xml is generated in CMakeLists.txt by

and then used by including the resulting file in your C++ implementation:

1// Include the encoded XML describing the operator class.
2// This is generated by CMake.
3#include "implement_an_operator_xml.h"