sofa-framework / sofa

Real-time multi-physics simulation with an emphasis on medical simulation.
https://www.sofa-framework.org
GNU Lesser General Public License v2.1
934 stars 312 forks source link

[Helper] A static alternative to OptionsGroup: SelectableItem #5062

Open alxbilger opened 1 month ago

alxbilger commented 1 month ago

SelectableItem is a new type that can replace OptionsGroup in some cases. The idea is the same: a list of keys + a selection of a key among the list. In OptionsGroup, everything is dynamic, whereas SelectableItem is designed to be static.

Disadvantages of OptionsGroup:

OptionsGroup foo{{"optionA", "optionB"}};
foo.setSelectedItem(12); //out of bounds
foo.setSelectedItem("optionC"); //this key does not exist

if (foo.getSelectedId() == 0) // 0 has no meaning here. We don't know what 0
    // refers to. In addition, the order of the options can change
{

}

In SelectableItem:

struct TestSelectableItem final : sofa::helper::SelectableItem<TestSelectableItem>
{
    static constexpr std::array s_items {
        sofa::helper::Item{"small", "small displacements"},
        sofa::helper::Item{"large", "large displacements"},
    };
};

The consequences:

static constexpr auto size = TestSelectableItem::numberOfItems();
static const auto& allKeys = TestSelectableItem::allKeysAsString();
static constexpr TestSelectableItem foo("large");

It allows to use the type in a constexpr context. For example:

  switch ( d_resolutionMethod.getValue() )
  {
      case ResolutionMethod("ProjectedGaussSeidel"):
      case ResolutionMethod("NonsmoothNonlinearConjugateGradient"):
      {
          buildSystem_matrixAssembly(cParams);
          break;
      }
      case ResolutionMethod("UnbuiltGaussSeidel"):
      {
          buildSystem_matrixFree(numConstraints);
          break;
      }
      default:
          msg_error() << "Wrong \"resolutionMethod\" given";
  }

There is compile-time check that any of the ResolutionMethod("ProjectedGaussSeidel"), ResolutionMethod("NonsmoothNonlinearConjugateGradient"), ResolutionMethod("UnbuiltGaussSeidel") exist. If it does not exist, it does not compile.

It is preferable to force the constexpr context:

static constexpr ResolutionMethod NonsmoothNonlinearConjugateGradient("NonsmoothNonlinearConjugateGradient");
if (d_resolutionMethod.getValue() != NonsmoothNonlinearConjugateGradient)
{

The compiler may not choose to use constexpr in:

if (d_resolutionMethod.getValue() != ResolutionMethod("NonsmoothNonlinearConjugateGradient"))
{

Therefore, no constexpr check if NonsmoothNonlinearConjugateGradient is not in the list. There is a runtime check though.

The type allows to deprecate a key if desired (I am thinking about the Data method in the FEM force fields).

There is also a description of each key. And the whole list (key + description) can be added easily in the description of the Data, hence in the documentation.

The major problem is about the GUI. More particularly, when a Data is read to be displayed in the GUI. Here, each of the SelectableItem is a new strong type. So there is no DataTypeName, DataTypeInfo etc for the new type. I tried to factorize with a common base class (BaseSelectableItem) but without success. I tried to define DataTypeName, DataTypeInfo etc for SelectedItem but without success. The consequence is that I cannot specialize a widget for this type of Data.


By submitting this pull request, I acknowledge that
I have read, understand, and agree SOFA Developer Certificate of Origin (DCO).


Reviewers will merge this pull-request only if

alxbilger commented 1 month ago

[ci-build][with-all-tests]

alxbilger commented 2 weeks ago

@damienmarchal I managed to return a common string for all types deriving from SelectableItem when calling Data::getValueTypeString. Now, the problem is with the dynamic_cast:

struct TestSelectableItem final : sofa::helper::SelectableItem<TestSelectableItem> {};
TestSelectableItem item;
sofa::Data<TestSelectableItem> data;
BaseData* baseData = &data;

Obviously dynamic_cast<BaseSelectableItem*>(&item) != nullptr, but dynamic_cast<Data<BaseSelectableItem>*>(&baseData)==nullptr.

The widget mechanism relies on dynamic_cast to go from a BaseData to a Data<T> (https://github.com/sofa-framework/sofa/blob/cda8e2306915cb2ae768ff6a283e7931dfa637db/Sofa/GUI/Qt/src/sofa/gui/qt/DataWidget.h#L69). Any idea?

damienmarchal commented 2 weeks ago

@alxbilger

I was trying to find a solution but looking at the code in DataWidget.cpp makes me notice:

DataWidget *DataWidget::CreateDataWidget(const DataWidget::CreatorArgument &dwarg)
{
    DataWidget *datawidget_ = nullptr;
    const std::string &widgetName=dwarg.data->getWidget();
    if (widgetName.empty())
        datawidget_ = DataWidgetFactory::CreateAnyObject(dwarg);
    else
        datawidget_ = DataWidgetFactory::CreateObject(widgetName, dwarg);

    return datawidget_;
}

And wonder if you try the getWidget() to short cut the dynamic_cast ?

alxbilger commented 2 weeks ago

@damienmarchal I believe that the dynamic_cast still happens in https://github.com/sofa-framework/sofa/blob/master/Sofa/GUI/Qt/src/sofa/gui/qt/DataWidget.h#L180

alxbilger commented 1 week ago

The type is now supported in both Qt and ImGui:

selectableItem

selectableItemQt