Operations based on a selection

In adding operations to modelbuilder v6, we want to improve on the drop-down list in v5. One thing we want to do is present operations that can be applied to the currently selected components. However, this seemingly simple task is not well-defined upon closer inspection. Some complicating factors are:

  • Operations specify components that are acceptable by providing a list of (resource type, filter) string tuples. The filter string is opaque to resource and operation managers; they ask individual resources to provide a C++ lambda that uses the string to determine whether a component owned by that resource is acceptable. With many operations, each of which may have many acceptable tuples, and many components in a selection, this can be expensive, especially since the filter string may need to be parsed before it can be used.
  • While it is fast to discriminate on the subclass of smtk::resource::Component (or equivalently, its typeinfo), we want to accommodate filtering on:
    • model entity types. Model vertices, edges, faces, volumes, etc. are all represented by instances of a single class (smtk::model::Entity) but we want to discriminate among them.
    • role. In the future we want to be able to assign any number of arbitrary string tags to components which specify their role in the model. Operations should be able to specify their inputs in terms of these roles. For example, model edges may represent rivers, roads, train tracks, and bike paths in a simulation; an operator may only work on rivers.
    • expressions. Even more complex filters are possible, either because attributes present on input components are used as discriminators or because the discriminator is a function of multiple components (e.g., an operator that cannot be applied to model volumes with different material attributes).
  • Another desire is to behave differently when an operator may be immediately applied because it does not accept further input (beyond the current selection), when an operator requires further input, or when an operator accepts further input but provides acceptable default values and thus may be applied immediately.

There are also things our current code does that may not be what we want:

  • Selections may be large and our current API is
     bool smtk::operation::Manager::availableOperations(smtk::resource::ComponentPtr component)
    which could be very expensive. The reason this is expensive is that each time it is called, this method causes resources to re-parse the filter/query strings (there may be several) that each operator provides.
  • We currently ignore restrictions each operator places on the number of components allowed. This would be a good way to terminate early and provide speedups, although it may also be constraining to users if it is difficult for them to select just the entities they are interested in.
  • We currently check all items of every definition in each operator’s attribute collection and probably don’t want to (because how would we assign components in the selection when there are multiple items?).
  • We currently don’t check associations allowed by any definition in each operator’s attribute collection, and should probably check only associations on the operator’s parameters() attribute definition.

How should we close the gap between what we have implemented and what we believe will be both effective for users and tractable for computers?

One potential scheme to accelerate things: have the operation manager keep a map from query tuples (resource type, filter) to C++ lambdas. When given a new selection, it evaluates each component in the selection against each lambda, filling out another map from query tuples to the number of matched components. Then loop over each operator and see which (if any) of its query tuples match.

using tuple = std::pair<std::string, std::string>;
using evaluator = std::function<bool(smtk::resource::ComponentPtr)>;

std::map<tuple, evaluator> m_availableOperatorFilters;
std::map<tuple, int> m_matchedOperatorFilters; // for the current selection
int m_currentSelectionSize; // so we know whether the entire selection matched a filter

This saves us from

  • having to re-parse the same query tuple for each component (or even for each selection change).
  • re-evaluating the same C++ lambda multiple times for different operators that accept the same input types.

However, it does complicate things. As operators are added/removed to/from the manager, both maps would need to be rebuilt completely.

Something like this is already being done in each Operation’s Metadata. We could expand the metadata’s implementation of this to handle tuples and have each map be filled lazily.

Before we go down that road, I think we should write a non-caching, generic version that simply loops over elements in a tuple and checks if they are acceptable inputs to an operator. If we see that this is a bottleneck for our use cases, then we can solve the problem.

I agree, although it seems highly probable given that we must apply it to every operator upon each selection. This is also something that seems like it should happen in a background thread lest we cause the application to stop while we deal with the new selection.

Another thing to consider: some of our current operations have an item named “model” that CMB v5 sets to be the active model if it is present. How do we deal with the concept of an active default model in addition to the active selection in a programmatic way?

  • Continue the practice of looking for any Component that expects an smtk::model::Model and set its default to the active model (it seems fragile and icky to rely on an application-specific behavior)?
  • Have operators that need a default model discover the active model somehow (but scripts or non-CMB applications might not have a default)?
  • Most operators exhibiting this pattern have an either/or behavior that is maybe a sign we should split them into multiple operators. Example: smtk/model/operators/EntityGroupOperation has no associations and several modes (create group, add/remove subgroup, add/remove cells).

For now CMB is the only one who has a concept of the active model right? If that’s the case, I vote for continuing the current practice.

And if in CMB6 we do not allow selections from a nonactive model then this problem becomes simple - find the model of the current selections. For operators like save/export smtk can just simply lists all models(resources?).

The problem with “continue as we currently do” is that it will complicate the functions which determine the available operations: the concept of an active resource and/or model is application-specific. And as you just pointed out, some operations (like save/export) make different assumptions, which mean adding exceptions into any availableOperations method we create. I don’t know that there’s an alternative, but it feels like we should look for one.